Refactor expense form and add PDF generation functionality

- Update expense form fields (merchant->establishmentName, amount->price)
- Add PDF generation with Puppeteer integration
- Create PDFOptionsModal component for export options
- Update expense form validation and UI layout
- Add server API endpoint for PDF generation
This commit is contained in:
Matt 2025-07-09 22:23:50 -04:00
parent b6d71faf5f
commit 893927d4b1
6 changed files with 901 additions and 189 deletions

View File

@ -7,7 +7,7 @@
> >
<v-card> <v-card>
<v-card-title class="d-flex align-center"> <v-card-title class="d-flex align-center">
<v-icon class="mr-2">mdi-receipt-text</v-icon> <v-icon class="mr-2">mdi-plus</v-icon>
<span>Add New Expense</span> <span>Add New Expense</span>
<v-spacer /> <v-spacer />
<v-btn <v-btn
@ -19,44 +19,35 @@
</v-card-title> </v-card-title>
<v-card-text> <v-card-text>
<v-form ref="form" @submit.prevent="saveExpense"> <v-form ref="form" @submit.prevent="handleSubmit">
<v-row> <v-row>
<!-- Merchant/Description --> <!-- Establishment Name -->
<v-col cols="12"> <v-col cols="12">
<v-text-field <v-text-field
v-model="expense.merchant" v-model="expense.establishmentName"
label="Merchant/Description" label="Establishment Name"
variant="outlined" variant="outlined"
:rules="[rules.required]" :rules="[rules.required]"
required required
placeholder="e.g., Shell, American Airlines, etc."
/> />
</v-col> </v-col>
<!-- Amount and Currency --> <!-- Price -->
<v-col cols="8"> <v-col cols="12" sm="6">
<v-text-field <v-text-field
v-model="expense.amount" v-model="expense.price"
label="Amount" label="Price"
type="number"
step="0.01"
variant="outlined" variant="outlined"
:rules="[rules.required, rules.positive]" :rules="[rules.required, rules.price]"
required
/>
</v-col>
<v-col cols="4">
<v-select
v-model="expense.currency"
:items="currencies"
label="Currency"
variant="outlined"
:rules="[rules.required]"
required required
placeholder="e.g., 59.95"
prepend-inner-icon="mdi-currency-eur"
/> />
</v-col> </v-col>
<!-- Category --> <!-- Category -->
<v-col cols="12" md="6"> <v-col cols="12" sm="6">
<v-select <v-select
v-model="expense.category" v-model="expense.category"
:items="categories" :items="categories"
@ -68,60 +59,49 @@
</v-col> </v-col>
<!-- Payer --> <!-- Payer -->
<v-col cols="12" md="6"> <v-col cols="12" sm="6">
<v-text-field <v-text-field
v-model="expense.payer" v-model="expense.payer"
label="Payer" label="Payer"
variant="outlined" variant="outlined"
:rules="[rules.required]" :rules="[rules.required]"
required required
placeholder="e.g., John, Mary, etc."
/>
</v-col>
<!-- Payment Method -->
<v-col cols="12" sm="6">
<v-select
v-model="expense.paymentMethod"
:items="paymentMethods"
label="Payment Method"
variant="outlined"
:rules="[rules.required]"
required
/> />
</v-col> </v-col>
<!-- Date --> <!-- Date -->
<v-col cols="12" md="6"> <v-col cols="12">
<v-text-field <v-text-field
v-model="expense.date" v-model="expense.date"
label="Date"
type="date" type="date"
label="Date"
variant="outlined" variant="outlined"
:rules="[rules.required]" :rules="[rules.required]"
required required
/> />
</v-col> </v-col>
<!-- Time --> <!-- Contents/Description -->
<v-col cols="12" md="6">
<v-text-field
v-model="expense.time"
label="Time"
type="time"
variant="outlined"
:rules="[rules.required]"
required
/>
</v-col>
<!-- Notes -->
<v-col cols="12"> <v-col cols="12">
<v-textarea <v-textarea
v-model="expense.notes" v-model="expense.contents"
label="Notes (Optional)" label="Description (optional)"
variant="outlined" variant="outlined"
rows="3" rows="3"
auto-grow placeholder="Additional details about the expense..."
/>
</v-col>
<!-- Receipt Upload -->
<v-col cols="12">
<v-file-input
v-model="expense.receipt"
label="Receipt Image (Optional)"
accept="image/*"
variant="outlined"
prepend-icon="mdi-camera"
show-size
/> />
</v-col> </v-col>
</v-row> </v-row>
@ -133,17 +113,19 @@
<v-btn <v-btn
@click="closeModal" @click="closeModal"
variant="text" variant="text"
:disabled="saving" :disabled="creating"
> >
Cancel Cancel
</v-btn> </v-btn>
<v-btn <v-btn
@click="saveExpense" @click="handleSubmit"
:disabled="creating"
color="primary" color="primary"
:loading="saving" :loading="creating"
:disabled="!isValid"
> >
Add Expense <v-icon v-if="!creating" class="mr-1">mdi-plus</v-icon>
Create Expense
</v-btn> </v-btn>
</v-card-actions> </v-card-actions>
</v-card> </v-card>
@ -151,82 +133,81 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, watch, nextTick } from 'vue'; import { ref, computed, watch } from 'vue';
import type { Expense } from '@/utils/types';
// Props
interface Props { interface Props {
modelValue: boolean; modelValue: boolean;
} }
interface Emits {
(e: 'update:modelValue', value: boolean): void;
(e: 'created', expense: Expense): void;
}
const props = defineProps<Props>(); const props = defineProps<Props>();
const emit = defineEmits<Emits>();
// Emits
const emit = defineEmits<{
'update:modelValue': [value: boolean];
'created': [expense: any];
}>();
// Computed dialog model
const dialog = computed({ const dialog = computed({
get: () => props.modelValue, get: () => props.modelValue,
set: (value) => emit('update:modelValue', value) set: (value) => emit('update:modelValue', value)
}); });
// Reactive state
const form = ref(); const form = ref();
const saving = ref(false); const creating = ref(false);
// Form data
const expense = ref({ const expense = ref({
merchant: '', establishmentName: '',
amount: '', price: '',
currency: 'EUR',
category: '', category: '',
payer: '', payer: '',
paymentMethod: '',
date: '', date: '',
time: '', contents: ''
notes: '',
receipt: null as File[] | null
}); });
// Form options // Form options
const currencies = ['EUR', 'USD', 'GBP', 'AUD', 'CAD', 'CHF', 'SEK', 'NOK', 'DKK']; const categories = [
const categories = ['Food/Drinks', 'Shop', 'Online', 'Transportation', 'Accommodation', 'Entertainment', 'Other']; 'Food/Drinks',
'Shop',
'Online',
'Other'
];
const paymentMethods = [
'Card',
'Cash',
'N/A'
];
// Validation rules // Validation rules
const rules = { const rules = {
required: (value: any) => !!value || 'This field is required', required: (value: string) => !!value || 'This field is required',
positive: (value: any) => { price: (value: string) => {
if (!value) return 'Price is required';
const num = parseFloat(value); const num = parseFloat(value);
return (!isNaN(num) && num > 0) || 'Amount must be positive'; if (isNaN(num) || num <= 0) return 'Please enter a valid price';
return true;
} }
}; };
// Computed properties
const isValid = computed(() => {
return !!(
expense.value.merchant &&
expense.value.amount &&
expense.value.currency &&
expense.value.category &&
expense.value.payer &&
expense.value.date &&
expense.value.time &&
parseFloat(expense.value.amount) > 0
);
});
// Methods // Methods
const closeModal = () => {
dialog.value = false;
resetForm();
};
const resetForm = () => { const resetForm = () => {
const now = new Date();
expense.value = { expense.value = {
merchant: '', establishmentName: '',
amount: '', price: '',
currency: 'EUR',
category: '', category: '',
payer: '', payer: '',
date: now.toISOString().slice(0, 10), paymentMethod: '',
time: now.toTimeString().slice(0, 5), date: '',
notes: '', contents: ''
receipt: null
}; };
if (form.value) { if (form.value) {
@ -234,89 +215,56 @@ const resetForm = () => {
} }
}; };
const closeModal = () => { const handleSubmit = async () => {
if (!saving.value) {
dialog.value = false;
}
};
const saveExpense = async () => {
if (!form.value) return; if (!form.value) return;
const { valid } = await form.value.validate(); const { valid } = await form.value.validate();
if (!valid) return; if (!valid) return;
saving.value = true; creating.value = true;
try { try {
// Combine date and time for the API // Create expense via API
const dateTime = `${expense.value.date}T${expense.value.time}:00`; const response = await $fetch<{
success: boolean;
// Prepare the expense data data?: any;
const expenseData = { message?: string;
"Establishment Name": expense.value.merchant, }>('/api/create-expense', {
Price: `${expense.value.currency}${expense.value.amount}`,
Category: expense.value.category,
Payer: expense.value.payer,
Time: dateTime,
Contents: expense.value.notes || null,
"Payment Method": "Card", // Default to Card for now
Paid: false,
currency: expense.value.currency
};
console.log('[ExpenseCreateModal] Creating expense:', expenseData);
// Call API to create expense
const response = await $fetch<Expense>('/api/create-expense', {
method: 'POST', method: 'POST',
body: expenseData body: {
'Establishment Name': expense.value.establishmentName,
'Price': expense.value.price,
'Category': expense.value.category,
'Payer': expense.value.payer,
'Payment Method': expense.value.paymentMethod,
'Time': expense.value.date,
'Contents': expense.value.contents
}
}); });
console.log('[ExpenseCreateModal] Expense created successfully:', response); if (response.success) {
emit('created', response.data);
// Emit the created event closeModal();
emit('created', response); }
// Close the modal
dialog.value = false;
} catch (error: any) { } catch (error: any) {
console.error('[ExpenseCreateModal] Error creating expense:', error); console.error('[ExpenseCreateModal] Error creating expense:', error);
// Handle error display here if needed
// Show error message (you might want to use a toast notification here)
alert('Failed to create expense. Please try again.');
} finally { } finally {
saving.value = false; creating.value = false;
} }
}; };
// Watch for modal open/close // Watch for modal open to set default date
watch(dialog, (newValue) => { watch(dialog, (isOpen) => {
if (newValue) { if (isOpen && !expense.value.date) {
// Reset form when modal opens expense.value.date = new Date().toISOString().slice(0, 10);
nextTick(() => {
resetForm();
});
} }
}); });
// Initialize form with current date/time
onMounted(() => {
resetForm();
});
</script> </script>
<style scoped> <style scoped>
.v-dialog > .v-card {
overflow: visible;
}
.v-form {
width: 100%;
}
.v-card-actions { .v-card-actions {
border-top: 1px solid rgba(0, 0, 0, 0.12); border-top: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
} }
</style> </style>

View File

@ -103,6 +103,19 @@
</template> </template>
</v-checkbox> </v-checkbox>
<v-checkbox
v-model="options.includeReceiptContents"
color="primary"
hide-details
>
<template #label>
<div>
<div class="font-weight-medium">Include Receipt Contents</div>
<div class="text-caption text-grey-darken-1">Show receipt description/contents in detail table</div>
</div>
</template>
</v-checkbox>
<v-checkbox <v-checkbox
v-model="options.includeProcessingFee" v-model="options.includeProcessingFee"
color="primary" color="primary"
@ -204,6 +217,7 @@ interface PDFOptions {
subheader: string; subheader: string;
groupBy: 'none' | 'payer' | 'category' | 'date'; groupBy: 'none' | 'payer' | 'category' | 'date';
includeReceipts: boolean; includeReceipts: boolean;
includeReceiptContents: boolean;
includeSummary: boolean; includeSummary: boolean;
includeDetails: boolean; includeDetails: boolean;
includeProcessingFee: boolean; includeProcessingFee: boolean;
@ -225,6 +239,7 @@ const options = ref<PDFOptions>({
subheader: '', subheader: '',
groupBy: 'payer', groupBy: 'payer',
includeReceipts: true, includeReceipts: true,
includeReceiptContents: true,
includeSummary: true, includeSummary: true,
includeDetails: true, includeDetails: true,
includeProcessingFee: true, includeProcessingFee: true,

677
package-lock.json generated
View File

@ -22,6 +22,7 @@
"nodemailer": "^7.0.3", "nodemailer": "^7.0.3",
"nuxt": "^3.15.4", "nuxt": "^3.15.4",
"nuxt-directus": "^5.7.0", "nuxt-directus": "^5.7.0",
"puppeteer": "^24.12.1",
"sharp": "^0.34.2", "sharp": "^0.34.2",
"v-phone-input": "^4.4.2", "v-phone-input": "^4.4.2",
"vue": "latest", "vue": "latest",
@ -3750,6 +3751,27 @@
"integrity": "sha512-8LduaNlMZGwdZ6qWrKlfa+2M4gahzFkprZiAt2TF8uS0qQgBizKXpXURqvTJ4WtmupWxaLqjRb2UCTe72mu+Aw==", "integrity": "sha512-8LduaNlMZGwdZ6qWrKlfa+2M4gahzFkprZiAt2TF8uS0qQgBizKXpXURqvTJ4WtmupWxaLqjRb2UCTe72mu+Aw==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/@puppeteer/browsers": {
"version": "2.10.5",
"resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.10.5.tgz",
"integrity": "sha512-eifa0o+i8dERnngJwKrfp3dEq7ia5XFyoqB17S4gK8GhsQE4/P8nxOfQSE0zQHxzzLo/cmF+7+ywEQ7wK7Fb+w==",
"license": "Apache-2.0",
"dependencies": {
"debug": "^4.4.1",
"extract-zip": "^2.0.1",
"progress": "^2.0.3",
"proxy-agent": "^6.5.0",
"semver": "^7.7.2",
"tar-fs": "^3.0.8",
"yargs": "^17.7.2"
},
"bin": {
"browsers": "lib/cjs/main-cli.js"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@rc-component/async-validator": { "node_modules/@rc-component/async-validator": {
"version": "5.0.4", "version": "5.0.4",
"resolved": "https://registry.npmjs.org/@rc-component/async-validator/-/async-validator-5.0.4.tgz", "resolved": "https://registry.npmjs.org/@rc-component/async-validator/-/async-validator-5.0.4.tgz",
@ -4466,6 +4488,12 @@
"tslib": "^2.8.0" "tslib": "^2.8.0"
} }
}, },
"node_modules/@tootallnate/quickjs-emscripten": {
"version": "0.23.0",
"resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz",
"integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==",
"license": "MIT"
},
"node_modules/@trysound/sax": { "node_modules/@trysound/sax": {
"version": "0.2.0", "version": "0.2.0",
"resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz", "resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz",
@ -4586,6 +4614,16 @@
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/yauzl": {
"version": "2.10.3",
"resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz",
"integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==",
"license": "MIT",
"optional": true,
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@unhead/dom": { "node_modules/@unhead/dom": {
"version": "1.11.18", "version": "1.11.18",
"resolved": "https://registry.npmjs.org/@unhead/dom/-/dom-1.11.18.tgz", "resolved": "https://registry.npmjs.org/@unhead/dom/-/dom-1.11.18.tgz",
@ -5445,6 +5483,18 @@
"node": ">=16.14.0" "node": ">=16.14.0"
} }
}, },
"node_modules/ast-types": {
"version": "0.13.4",
"resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz",
"integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==",
"license": "MIT",
"dependencies": {
"tslib": "^2.0.1"
},
"engines": {
"node": ">=4"
}
},
"node_modules/ast-walker-scope": { "node_modules/ast-walker-scope": {
"version": "0.6.2", "version": "0.6.2",
"resolved": "https://registry.npmjs.org/ast-walker-scope/-/ast-walker-scope-0.6.2.tgz", "resolved": "https://registry.npmjs.org/ast-walker-scope/-/ast-walker-scope-0.6.2.tgz",
@ -5646,6 +5696,71 @@
"license": "Apache-2.0", "license": "Apache-2.0",
"optional": true "optional": true
}, },
"node_modules/bare-fs": {
"version": "4.1.6",
"resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.1.6.tgz",
"integrity": "sha512-25RsLF33BqooOEFNdMcEhMpJy8EoR88zSMrnOQOaM3USnOK2VmaJ1uaQEwPA6AQjrv1lXChScosN6CzbwbO9OQ==",
"license": "Apache-2.0",
"optional": true,
"dependencies": {
"bare-events": "^2.5.4",
"bare-path": "^3.0.0",
"bare-stream": "^2.6.4"
},
"engines": {
"bare": ">=1.16.0"
},
"peerDependencies": {
"bare-buffer": "*"
},
"peerDependenciesMeta": {
"bare-buffer": {
"optional": true
}
}
},
"node_modules/bare-os": {
"version": "3.6.1",
"resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.6.1.tgz",
"integrity": "sha512-uaIjxokhFidJP+bmmvKSgiMzj2sV5GPHaZVAIktcxcpCyBFFWO+YlikVAdhmUo2vYFvFhOXIAlldqV29L8126g==",
"license": "Apache-2.0",
"optional": true,
"engines": {
"bare": ">=1.14.0"
}
},
"node_modules/bare-path": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.0.tgz",
"integrity": "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==",
"license": "Apache-2.0",
"optional": true,
"dependencies": {
"bare-os": "^3.0.1"
}
},
"node_modules/bare-stream": {
"version": "2.6.5",
"resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.6.5.tgz",
"integrity": "sha512-jSmxKJNJmHySi6hC42zlZnq00rga4jjxcgNZjY9N5WlOe/iOoGRtdwGsHzQv2RlH2KOYMwGUXhf2zXd32BA9RA==",
"license": "Apache-2.0",
"optional": true,
"dependencies": {
"streamx": "^2.21.0"
},
"peerDependencies": {
"bare-buffer": "*",
"bare-events": "*"
},
"peerDependenciesMeta": {
"bare-buffer": {
"optional": true
},
"bare-events": {
"optional": true
}
}
},
"node_modules/base64-js": { "node_modules/base64-js": {
"version": "1.5.1", "version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
@ -5666,6 +5781,15 @@
], ],
"license": "MIT" "license": "MIT"
}, },
"node_modules/basic-ftp": {
"version": "5.0.5",
"resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.0.5.tgz",
"integrity": "sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/binary-extensions": { "node_modules/binary-extensions": {
"version": "2.3.0", "version": "2.3.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
@ -5963,6 +6087,15 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/callsites": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
"integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/caniuse-api": { "node_modules/caniuse-api": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/caniuse-api/-/caniuse-api-3.0.0.tgz", "resolved": "https://registry.npmjs.org/caniuse-api/-/caniuse-api-3.0.0.tgz",
@ -6053,6 +6186,19 @@
"node": ">=10" "node": ">=10"
} }
}, },
"node_modules/chromium-bidi": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-5.1.0.tgz",
"integrity": "sha512-9MSRhWRVoRPDG0TgzkHrshFSJJNZzfY5UFqUMuksg7zL1yoZIZ3jLB0YAgHclbiAxPI86pBnwDX1tbzoiV8aFw==",
"license": "Apache-2.0",
"dependencies": {
"mitt": "^3.0.1",
"zod": "^3.24.1"
},
"peerDependencies": {
"devtools-protocol": "*"
}
},
"node_modules/citty": { "node_modules/citty": {
"version": "0.1.6", "version": "0.1.6",
"resolved": "https://registry.npmjs.org/citty/-/citty-0.1.6.tgz", "resolved": "https://registry.npmjs.org/citty/-/citty-0.1.6.tgz",
@ -6394,6 +6540,50 @@
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/cosmiconfig": {
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz",
"integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==",
"license": "MIT",
"dependencies": {
"env-paths": "^2.2.1",
"import-fresh": "^3.3.0",
"js-yaml": "^4.1.0",
"parse-json": "^5.2.0"
},
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/sponsors/d-fischer"
},
"peerDependencies": {
"typescript": ">=4.9.5"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
}
},
"node_modules/cosmiconfig/node_modules/parse-json": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz",
"integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==",
"license": "MIT",
"dependencies": {
"@babel/code-frame": "^7.0.0",
"error-ex": "^1.3.1",
"json-parse-even-better-errors": "^2.3.0",
"lines-and-columns": "^1.1.6"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/countries-list": { "node_modules/countries-list": {
"version": "3.1.1", "version": "3.1.1",
"resolved": "https://registry.npmjs.org/countries-list/-/countries-list-3.1.1.tgz", "resolved": "https://registry.npmjs.org/countries-list/-/countries-list-3.1.1.tgz",
@ -6692,6 +6882,15 @@
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/data-uri-to-buffer": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz",
"integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==",
"license": "MIT",
"engines": {
"node": ">= 14"
}
},
"node_modules/data-view-buffer": { "node_modules/data-view-buffer": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz",
@ -6796,9 +6995,9 @@
} }
}, },
"node_modules/debug": { "node_modules/debug": {
"version": "4.4.0", "version": "4.4.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
"integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"ms": "^2.1.3" "ms": "^2.1.3"
@ -6907,6 +7106,20 @@
"integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==", "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/degenerator": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz",
"integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==",
"license": "MIT",
"dependencies": {
"ast-types": "^0.13.4",
"escodegen": "^2.1.0",
"esprima": "^4.0.1"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/denque": { "node_modules/denque": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz",
@ -6959,6 +7172,12 @@
"integrity": "sha512-maua5KUiapvEwiEAe+XnlZ3Rh0GD+qI1J/nb9vrJc3muPXvcF/8gXYTWF76+5DAqHyDUtOIImEuo0YKE9mshVw==", "integrity": "sha512-maua5KUiapvEwiEAe+XnlZ3Rh0GD+qI1J/nb9vrJc3muPXvcF/8gXYTWF76+5DAqHyDUtOIImEuo0YKE9mshVw==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/devtools-protocol": {
"version": "0.0.1464554",
"resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1464554.tgz",
"integrity": "sha512-CAoP3lYfwAGQTaAXYvA6JZR0fjGUb7qec1qf4mToyoH2TZgUFeIqYcjh6f9jNuhHfuZiEdH+PONHYrLhRQX6aw==",
"license": "BSD-3-Clause"
},
"node_modules/dezalgo": { "node_modules/dezalgo": {
"version": "1.0.4", "version": "1.0.4",
"resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz",
@ -7150,6 +7369,15 @@
"node": ">=8.10.0" "node": ">=8.10.0"
} }
}, },
"node_modules/end-of-stream": {
"version": "1.4.5",
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
"integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==",
"license": "MIT",
"dependencies": {
"once": "^1.4.0"
}
},
"node_modules/enhanced-resolve": { "node_modules/enhanced-resolve": {
"version": "5.18.0", "version": "5.18.0",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.0.tgz", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.0.tgz",
@ -7175,6 +7403,30 @@
"url": "https://github.com/fb55/entities?sponsor=1" "url": "https://github.com/fb55/entities?sponsor=1"
} }
}, },
"node_modules/env-paths": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz",
"integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/error-ex": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
"integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==",
"license": "MIT",
"dependencies": {
"is-arrayish": "^0.2.1"
}
},
"node_modules/error-ex/node_modules/is-arrayish": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
"integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==",
"license": "MIT"
},
"node_modules/error-stack-parser-es": { "node_modules/error-stack-parser-es": {
"version": "0.1.5", "version": "0.1.5",
"resolved": "https://registry.npmjs.org/error-stack-parser-es/-/error-stack-parser-es-0.1.5.tgz", "resolved": "https://registry.npmjs.org/error-stack-parser-es/-/error-stack-parser-es-0.1.5.tgz",
@ -7390,6 +7642,59 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/escodegen": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz",
"integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==",
"license": "BSD-2-Clause",
"dependencies": {
"esprima": "^4.0.1",
"estraverse": "^5.2.0",
"esutils": "^2.0.2"
},
"bin": {
"escodegen": "bin/escodegen.js",
"esgenerate": "bin/esgenerate.js"
},
"engines": {
"node": ">=6.0"
},
"optionalDependencies": {
"source-map": "~0.6.1"
}
},
"node_modules/escodegen/node_modules/source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"license": "BSD-3-Clause",
"optional": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/esprima": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
"integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
"license": "BSD-2-Clause",
"bin": {
"esparse": "bin/esparse.js",
"esvalidate": "bin/esvalidate.js"
},
"engines": {
"node": ">=4"
}
},
"node_modules/estraverse": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
"integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=4.0"
}
},
"node_modules/estree-walker": { "node_modules/estree-walker": {
"version": "3.0.3", "version": "3.0.3",
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
@ -7482,6 +7787,41 @@
"integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/extract-zip": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz",
"integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==",
"license": "BSD-2-Clause",
"dependencies": {
"debug": "^4.1.1",
"get-stream": "^5.1.0",
"yauzl": "^2.10.0"
},
"bin": {
"extract-zip": "cli.js"
},
"engines": {
"node": ">= 10.17.0"
},
"optionalDependencies": {
"@types/yauzl": "^2.9.1"
}
},
"node_modules/extract-zip/node_modules/get-stream": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz",
"integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==",
"license": "MIT",
"dependencies": {
"pump": "^3.0.0"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/fast-deep-equal": { "node_modules/fast-deep-equal": {
"version": "3.1.3", "version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
@ -7568,6 +7908,15 @@
"reusify": "^1.0.4" "reusify": "^1.0.4"
} }
}, },
"node_modules/fd-slicer": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz",
"integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==",
"license": "MIT",
"dependencies": {
"pend": "~1.2.0"
}
},
"node_modules/fdir": { "node_modules/fdir": {
"version": "6.4.3", "version": "6.4.3",
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.3.tgz", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.3.tgz",
@ -8054,6 +8403,20 @@
"url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
} }
}, },
"node_modules/get-uri": {
"version": "6.0.5",
"resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.5.tgz",
"integrity": "sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg==",
"license": "MIT",
"dependencies": {
"basic-ftp": "^5.0.2",
"data-uri-to-buffer": "^6.0.2",
"debug": "^4.3.4"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/giget": { "node_modules/giget": {
"version": "1.2.4", "version": "1.2.4",
"resolved": "https://registry.npmjs.org/giget/-/giget-1.2.4.tgz", "resolved": "https://registry.npmjs.org/giget/-/giget-1.2.4.tgz",
@ -8435,6 +8798,19 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/http-proxy-agent": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz",
"integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==",
"license": "MIT",
"dependencies": {
"agent-base": "^7.1.0",
"debug": "^4.3.4"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/http-shutdown": { "node_modules/http-shutdown": {
"version": "1.2.2", "version": "1.2.2",
"resolved": "https://registry.npmjs.org/http-shutdown/-/http-shutdown-1.2.2.tgz", "resolved": "https://registry.npmjs.org/http-shutdown/-/http-shutdown-1.2.2.tgz",
@ -8562,6 +8938,31 @@
"integrity": "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==", "integrity": "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/import-fresh": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
"integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==",
"license": "MIT",
"dependencies": {
"parent-module": "^1.0.0",
"resolve-from": "^4.0.0"
},
"engines": {
"node": ">=6"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/import-fresh/node_modules/resolve-from": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
"integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
"license": "MIT",
"engines": {
"node": ">=4"
}
},
"node_modules/importx": { "node_modules/importx": {
"version": "0.4.4", "version": "0.4.4",
"resolved": "https://registry.npmjs.org/importx/-/importx-0.4.4.tgz", "resolved": "https://registry.npmjs.org/importx/-/importx-0.4.4.tgz",
@ -9133,6 +9534,19 @@
"url": "https://opencollective.com/ioredis" "url": "https://opencollective.com/ioredis"
} }
}, },
"node_modules/ip-address": {
"version": "9.0.5",
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz",
"integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==",
"license": "MIT",
"dependencies": {
"jsbn": "1.1.0",
"sprintf-js": "^1.1.3"
},
"engines": {
"node": ">= 12"
}
},
"node_modules/ipaddr.js": { "node_modules/ipaddr.js": {
"version": "2.2.0", "version": "2.2.0",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.2.0.tgz", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.2.0.tgz",
@ -9806,6 +10220,12 @@
"js-yaml": "bin/js-yaml.js" "js-yaml": "bin/js-yaml.js"
} }
}, },
"node_modules/jsbn": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz",
"integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==",
"license": "MIT"
},
"node_modules/jsesc": { "node_modules/jsesc": {
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
@ -9818,6 +10238,12 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/json-parse-even-better-errors": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
"integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==",
"license": "MIT"
},
"node_modules/json-schema": { "node_modules/json-schema": {
"version": "0.4.0", "version": "0.4.0",
"resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz",
@ -10015,6 +10441,12 @@
"url": "https://github.com/sponsors/antonk52" "url": "https://github.com/sponsors/antonk52"
} }
}, },
"node_modules/lines-and-columns": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
"integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
"license": "MIT"
},
"node_modules/linkify-it": { "node_modules/linkify-it": {
"version": "5.0.0", "version": "5.0.0",
"resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz", "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz",
@ -10493,6 +10925,15 @@
"integrity": "sha512-9ca1h0Xjvo9bEkE4UOxgAzLV0jHKe6LMaxo37ND2DAhhAtd0j8pR1Wxz+/goMrZO8AEZTWCmyaOsFI/W5AdpCQ==", "integrity": "sha512-9ca1h0Xjvo9bEkE4UOxgAzLV0jHKe6LMaxo37ND2DAhhAtd0j8pR1Wxz+/goMrZO8AEZTWCmyaOsFI/W5AdpCQ==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/netmask": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/netmask/-/netmask-2.0.2.tgz",
"integrity": "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==",
"license": "MIT",
"engines": {
"node": ">= 0.4.0"
}
},
"node_modules/nitropack": { "node_modules/nitropack": {
"version": "2.10.4", "version": "2.10.4",
"resolved": "https://registry.npmjs.org/nitropack/-/nitropack-2.10.4.tgz", "resolved": "https://registry.npmjs.org/nitropack/-/nitropack-2.10.4.tgz",
@ -11175,6 +11616,38 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/pac-proxy-agent": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz",
"integrity": "sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==",
"license": "MIT",
"dependencies": {
"@tootallnate/quickjs-emscripten": "^0.23.0",
"agent-base": "^7.1.2",
"debug": "^4.3.4",
"get-uri": "^6.0.1",
"http-proxy-agent": "^7.0.0",
"https-proxy-agent": "^7.0.6",
"pac-resolver": "^7.0.1",
"socks-proxy-agent": "^8.0.5"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/pac-resolver": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz",
"integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==",
"license": "MIT",
"dependencies": {
"degenerator": "^5.0.0",
"netmask": "^2.0.2"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/package-json-from-dist": { "node_modules/package-json-from-dist": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
@ -11202,6 +11675,18 @@
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
"license": "(MIT AND Zlib)" "license": "(MIT AND Zlib)"
}, },
"node_modules/parent-module": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
"integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
"license": "MIT",
"dependencies": {
"callsites": "^3.0.0"
},
"engines": {
"node": ">=6"
}
},
"node_modules/parse-git-config": { "node_modules/parse-git-config": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/parse-git-config/-/parse-git-config-3.0.0.tgz", "resolved": "https://registry.npmjs.org/parse-git-config/-/parse-git-config-3.0.0.tgz",
@ -11355,6 +11840,12 @@
"url": "https://ko-fi.com/killymxi" "url": "https://ko-fi.com/killymxi"
} }
}, },
"node_modules/pend": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz",
"integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==",
"license": "MIT"
},
"node_modules/perfect-debounce": { "node_modules/perfect-debounce": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz", "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz",
@ -11970,6 +12461,15 @@
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/progress": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
"integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==",
"license": "MIT",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/prompts": { "node_modules/prompts": {
"version": "2.4.2", "version": "2.4.2",
"resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz",
@ -12001,6 +12501,50 @@
"integrity": "sha512-/XJ368cyBJ7fzLMwLKv1e4vLxOju2MNAIokcr7meSaNcVbWz/CPcW22cP04mwxOErdA5mwjA8Q6w/cdAQxVn7Q==", "integrity": "sha512-/XJ368cyBJ7fzLMwLKv1e4vLxOju2MNAIokcr7meSaNcVbWz/CPcW22cP04mwxOErdA5mwjA8Q6w/cdAQxVn7Q==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/proxy-agent": {
"version": "6.5.0",
"resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.5.0.tgz",
"integrity": "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==",
"license": "MIT",
"dependencies": {
"agent-base": "^7.1.2",
"debug": "^4.3.4",
"http-proxy-agent": "^7.0.1",
"https-proxy-agent": "^7.0.6",
"lru-cache": "^7.14.1",
"pac-proxy-agent": "^7.1.0",
"proxy-from-env": "^1.1.0",
"socks-proxy-agent": "^8.0.5"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/proxy-agent/node_modules/lru-cache": {
"version": "7.18.3",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz",
"integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==",
"license": "ISC",
"engines": {
"node": ">=12"
}
},
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
"license": "MIT"
},
"node_modules/pump": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz",
"integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==",
"license": "MIT",
"dependencies": {
"end-of-stream": "^1.1.0",
"once": "^1.3.1"
}
},
"node_modules/punycode": { "node_modules/punycode": {
"version": "2.3.1", "version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
@ -12019,6 +12563,44 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/puppeteer": {
"version": "24.12.1",
"resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-24.12.1.tgz",
"integrity": "sha512-+vvwl+Xo4z5uXLLHG+XW8uXnUXQ62oY6KU6bEFZJvHWLutbmv5dw9A/jcMQ0fqpQdLydHmK0Uy7/9Ilj8ufwSQ==",
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
"@puppeteer/browsers": "2.10.5",
"chromium-bidi": "5.1.0",
"cosmiconfig": "^9.0.0",
"devtools-protocol": "0.0.1464554",
"puppeteer-core": "24.12.1",
"typed-query-selector": "^2.12.0"
},
"bin": {
"puppeteer": "lib/cjs/puppeteer/node/cli.js"
},
"engines": {
"node": ">=18"
}
},
"node_modules/puppeteer-core": {
"version": "24.12.1",
"resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.12.1.tgz",
"integrity": "sha512-8odp6d3ERKBa3BAVaYWXn95UxQv3sxvP1reD+xZamaX6ed8nCykhwlOiHSaHR9t/MtmIB+rJmNencI6Zy4Gxvg==",
"license": "Apache-2.0",
"dependencies": {
"@puppeteer/browsers": "2.10.5",
"chromium-bidi": "5.1.0",
"debug": "^4.4.1",
"devtools-protocol": "0.0.1464554",
"typed-query-selector": "^2.12.0",
"ws": "^8.18.3"
},
"engines": {
"node": ">=18"
}
},
"node_modules/query-string": { "node_modules/query-string": {
"version": "7.1.3", "version": "7.1.3",
"resolved": "https://registry.npmjs.org/query-string/-/query-string-7.1.3.tgz", "resolved": "https://registry.npmjs.org/query-string/-/query-string-7.1.3.tgz",
@ -13904,12 +14486,50 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/smart-buffer": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz",
"integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==",
"license": "MIT",
"engines": {
"node": ">= 6.0.0",
"npm": ">= 3.0.0"
}
},
"node_modules/smob": { "node_modules/smob": {
"version": "1.5.0", "version": "1.5.0",
"resolved": "https://registry.npmjs.org/smob/-/smob-1.5.0.tgz", "resolved": "https://registry.npmjs.org/smob/-/smob-1.5.0.tgz",
"integrity": "sha512-g6T+p7QO8npa+/hNx9ohv1E5pVCmWrVCUzUXJyLdMmftX6ER0oiWY/w9knEonLpnOp6b6FenKnMfR8gqwWdwig==", "integrity": "sha512-g6T+p7QO8npa+/hNx9ohv1E5pVCmWrVCUzUXJyLdMmftX6ER0oiWY/w9knEonLpnOp6b6FenKnMfR8gqwWdwig==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/socks": {
"version": "2.8.5",
"resolved": "https://registry.npmjs.org/socks/-/socks-2.8.5.tgz",
"integrity": "sha512-iF+tNDQla22geJdTyJB1wM/qrX9DMRwWrciEPwWLPRWAUEM8sQiyxgckLxWT1f7+9VabJS0jTGGr4QgBuvi6Ww==",
"license": "MIT",
"dependencies": {
"ip-address": "^9.0.5",
"smart-buffer": "^4.2.0"
},
"engines": {
"node": ">= 10.0.0",
"npm": ">= 3.0.0"
}
},
"node_modules/socks-proxy-agent": {
"version": "8.0.5",
"resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz",
"integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==",
"license": "MIT",
"dependencies": {
"agent-base": "^7.1.2",
"debug": "^4.3.4",
"socks": "^2.8.3"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/source-map": { "node_modules/source-map": {
"version": "0.7.4", "version": "0.7.4",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz",
@ -13972,6 +14592,12 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/sprintf-js": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz",
"integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==",
"license": "BSD-3-Clause"
},
"node_modules/standard-as-callback": { "node_modules/standard-as-callback": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz", "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz",
@ -14388,6 +15014,20 @@
"node": ">=10" "node": ">=10"
} }
}, },
"node_modules/tar-fs": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.0.tgz",
"integrity": "sha512-5Mty5y/sOF1YWj1J6GiBodjlDc05CUR8PKXrsnFAiSG0xA+GHeWLovaZPYUDXkH/1iKRf2+M5+OrRgzC7O9b7w==",
"license": "MIT",
"dependencies": {
"pump": "^3.0.0",
"tar-stream": "^3.1.5"
},
"optionalDependencies": {
"bare-fs": "^4.0.1",
"bare-path": "^3.0.0"
}
},
"node_modules/tar-stream": { "node_modules/tar-stream": {
"version": "3.1.7", "version": "3.1.7",
"resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz", "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz",
@ -15146,6 +15786,12 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/typed-query-selector": {
"version": "2.12.0",
"resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.0.tgz",
"integrity": "sha512-SbklCd1F0EiZOyPiW192rrHZzZ5sBijB6xM+cpmrwDqObvdtunOHHIk9fCGsoK5JVIYXoyEp4iEdE3upFH3PAg==",
"license": "MIT"
},
"node_modules/typescript": { "node_modules/typescript": {
"version": "5.7.3", "version": "5.7.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz",
@ -16976,9 +17622,9 @@
"license": "ISC" "license": "ISC"
}, },
"node_modules/ws": { "node_modules/ws": {
"version": "8.18.0", "version": "8.18.3",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
"integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==",
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=10.0.0" "node": ">=10.0.0"
@ -17078,6 +17724,25 @@
"node": ">=12" "node": ">=12"
} }
}, },
"node_modules/yauzl": {
"version": "2.10.0",
"resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz",
"integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==",
"license": "MIT",
"dependencies": {
"buffer-crc32": "~0.2.3",
"fd-slicer": "~1.1.0"
}
},
"node_modules/yauzl/node_modules/buffer-crc32": {
"version": "0.2.13",
"resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz",
"integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==",
"license": "MIT",
"engines": {
"node": "*"
}
},
"node_modules/zhead": { "node_modules/zhead": {
"version": "2.2.4", "version": "2.2.4",
"resolved": "https://registry.npmjs.org/zhead/-/zhead-2.2.4.tgz", "resolved": "https://registry.npmjs.org/zhead/-/zhead-2.2.4.tgz",

View File

@ -24,6 +24,7 @@
"nodemailer": "^7.0.3", "nodemailer": "^7.0.3",
"nuxt": "^3.15.4", "nuxt": "^3.15.4",
"nuxt-directus": "^5.7.0", "nuxt-directus": "^5.7.0",
"puppeteer": "^24.12.1",
"sharp": "^0.34.2", "sharp": "^0.34.2",
"v-phone-input": "^4.4.2", "v-phone-input": "^4.4.2",
"vue": "latest", "vue": "latest",

View File

@ -504,23 +504,26 @@ const generatePDF = async (options: any) => {
}); });
if (response.success && response.data) { if (response.success && response.data) {
// For now, create HTML file instead of PDF since we're generating HTML content // Decode base64 PDF content
const htmlContent = atob(response.data.content); // Decode base64 const pdfContent = atob(response.data.content);
const blob = new Blob([htmlContent], { type: 'text/html' });
// Convert to byte array
const byteNumbers = new Array(pdfContent.length);
for (let i = 0; i < pdfContent.length; i++) {
byteNumbers[i] = pdfContent.charCodeAt(i);
}
const byteArray = new Uint8Array(byteNumbers);
// Create PDF blob and download
const blob = new Blob([byteArray], { type: 'application/pdf' });
const url = window.URL.createObjectURL(blob); const url = window.URL.createObjectURL(blob);
const a = document.createElement('a'); const a = document.createElement('a');
a.href = url; a.href = url;
a.download = `${options.documentName || 'expenses'}.html`; a.download = response.data.filename;
a.click(); a.click();
window.URL.revokeObjectURL(url); window.URL.revokeObjectURL(url);
// Also open in new tab for immediate viewing console.log('[expenses] PDF downloaded successfully:', response.data.filename);
const newTab = window.open();
if (newTab) {
newTab.document.open();
newTab.document.write(htmlContent);
newTab.document.close();
}
} }
showPDFModal.value = false; showPDFModal.value = false;

View File

@ -3,12 +3,14 @@ import { getExpenseById } from '@/server/utils/nocodb';
import { processExpenseWithCurrency } from '@/server/utils/currency'; import { processExpenseWithCurrency } from '@/server/utils/currency';
import { createError } from 'h3'; import { createError } from 'h3';
import { formatDate } from '@/utils/dateUtils'; import { formatDate } from '@/utils/dateUtils';
import puppeteer from 'puppeteer';
interface PDFOptions { interface PDFOptions {
documentName: string; documentName: string;
subheader?: string; subheader?: string;
groupBy: 'none' | 'payer' | 'category' | 'date'; groupBy: 'none' | 'payer' | 'category' | 'date';
includeReceipts: boolean; includeReceipts: boolean;
includeReceiptContents: boolean;
includeSummary: boolean; includeSummary: boolean;
includeDetails: boolean; includeDetails: boolean;
pageFormat: 'A4' | 'Letter' | 'Legal'; pageFormat: 'A4' | 'Letter' | 'Legal';
@ -77,19 +79,22 @@ export default defineEventHandler(async (event) => {
console.log('[expenses/generate-pdf] Successfully calculated totals:', totals); console.log('[expenses/generate-pdf] Successfully calculated totals:', totals);
console.log('[expenses/generate-pdf] Options received:', options); console.log('[expenses/generate-pdf] Options received:', options);
// Generate PDF content // Generate HTML content
const pdfContent = generatePDFContent(expenses, options, totals); const htmlContent = generateHTMLContent(expenses, options, totals);
// Convert HTML to PDF using Puppeteer
const pdfBuffer = await generatePDFFromHTML(htmlContent, options);
// Return PDF as base64 for download // Return PDF as base64 for download
const pdfBase64 = Buffer.from(pdfContent).toString('base64'); const pdfBase64 = pdfBuffer.toString('base64');
return { return {
success: true, success: true,
data: { data: {
filename: `${options.documentName.replace(/[^a-zA-Z0-9]/g, '_')}.pdf`, filename: `${options.documentName.replace(/[^a-zA-Z0-9\-_\s]/g, '_')}.pdf`,
content: pdfBase64, content: pdfBase64,
mimeType: 'application/pdf', mimeType: 'application/pdf',
size: pdfContent.length size: pdfBuffer.length
} }
}; };
@ -132,7 +137,7 @@ function getGroupingLabel(groupBy: string): string {
} }
} }
function generatePDFContent(expenses: Expense[], options: PDFOptions, totals: any): string { function generateHTMLContent(expenses: Expense[], options: PDFOptions, totals: any): string {
// Generate HTML content that can be converted to PDF // Generate HTML content that can be converted to PDF
const html = ` const html = `
<!DOCTYPE html> <!DOCTYPE html>
@ -197,7 +202,7 @@ function generateExpenseTable(expenses: Expense[], options: PDFOptions): string
<th>Payer</th> <th>Payer</th>
<th>Amount</th> <th>Amount</th>
<th>Payment Method</th> <th>Payment Method</th>
${options.includeDetails ? '<th>Description</th>' : ''} ${options.includeReceiptContents ? '<th>Description</th>' : ''}
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@ -219,7 +224,7 @@ function generateExpenseTable(expenses: Expense[], options: PDFOptions): string
// Group header // Group header
tableHTML += ` tableHTML += `
<tr class="group-header"> <tr class="group-header">
<td colspan="${options.includeDetails ? '7' : '6'}">${groupKey} (${groupExpenses.length} expenses - ${groupTotal.toFixed(2)})</td> <td colspan="${options.includeReceiptContents ? '7' : '6'}">${groupKey} (${groupExpenses.length} expenses - ${groupTotal.toFixed(2)})</td>
</tr> </tr>
`; `;
@ -250,7 +255,7 @@ function generateExpenseRow(expense: Expense, options: PDFOptions): string {
<td>${expense.Payer || 'N/A'}</td> <td>${expense.Payer || 'N/A'}</td>
<td>${expense.PriceNumber ? expense.PriceNumber.toFixed(2) : '0.00'}</td> <td>${expense.PriceNumber ? expense.PriceNumber.toFixed(2) : '0.00'}</td>
<td>${expense['Payment Method'] || 'N/A'}</td> <td>${expense['Payment Method'] || 'N/A'}</td>
${options.includeDetails ? `<td>${description}</td>` : ''} ${options.includeReceiptContents ? `<td>${description}</td>` : ''}
</tr> </tr>
`; `;
} }
@ -281,3 +286,78 @@ function groupExpenses(expenses: Expense[], groupBy: string): Record<string, Exp
return groups; return groups;
} }
async function generatePDFFromHTML(htmlContent: string, options: PDFOptions): Promise<Buffer> {
let browser;
try {
console.log('[expenses/generate-pdf] Launching Puppeteer browser...');
// Launch browser with optimized settings
browser = await puppeteer.launch({
headless: true,
args: [
'--no-sandbox',
'--disable-setuid-sandbox',
'--disable-dev-shm-usage',
'--disable-accelerated-2d-canvas',
'--no-first-run',
'--no-zygote',
'--disable-gpu'
]
});
const page = await browser.newPage();
// Set content with proper encoding
await page.setContent(htmlContent, {
waitUntil: 'networkidle0',
timeout: 30000
});
// Get page format dimensions
const format = getPageFormat(options.pageFormat);
console.log('[expenses/generate-pdf] Generating PDF with format:', format);
// Generate PDF with proper options
const pdfUint8Array = await page.pdf({
format: format.format,
printBackground: true,
margin: {
top: '20mm',
right: '15mm',
bottom: '20mm',
left: '15mm'
},
preferCSSPageSize: true
});
// Convert Uint8Array to Buffer
const pdfBuffer = Buffer.from(pdfUint8Array);
console.log('[expenses/generate-pdf] PDF generated successfully, size:', pdfBuffer.length, 'bytes');
return pdfBuffer;
} catch (error: any) {
console.error('[expenses/generate-pdf] Puppeteer error:', error);
throw new Error(`PDF generation failed: ${error?.message || 'Unknown error'}`);
} finally {
if (browser) {
await browser.close();
}
}
}
function getPageFormat(pageFormat: string): { format: any } {
switch (pageFormat) {
case 'Letter':
return { format: 'letter' };
case 'Legal':
return { format: 'legal' };
case 'A4':
default:
return { format: 'a4' };
}
}