This commit is contained in:
Thomas Peterson 2026-01-08 09:48:02 +01:00
parent 7e46c1fc5d
commit 3a0019a1b9
17 changed files with 1060 additions and 93 deletions

View File

@ -474,7 +474,7 @@ namespace Symfony\Component\DependencyInjection\Loader\Configurator;
* datetime?: array{
* default_format?: scalar|null, // Default: "Y-m-d\\TH:i:sP"
* default_deserialization_formats?: list<scalar|null>,
* default_timezone?: scalar|null, // Default: "UTC"
* default_timezone?: scalar|null, // Default: "Europe/Berlin"
* cdata?: scalar|null, // Default: true
* },
* array_collection?: array{
@ -574,7 +574,7 @@ namespace Symfony\Component\DependencyInjection\Loader\Configurator;
* datetime?: array{
* default_format?: scalar|null, // Default: "Y-m-d\\TH:i:sP"
* default_deserialization_formats?: list<scalar|null>,
* default_timezone?: scalar|null, // Default: "UTC"
* default_timezone?: scalar|null, // Default: "Europe/Berlin"
* cdata?: scalar|null, // Default: true
* },
* array_collection?: array{

View File

@ -8,6 +8,7 @@ class Lock extends Event
{
/** @var string */
protected $contact;
public function getType()
{
return 'contact_lock';
@ -15,13 +16,13 @@ class Lock extends Event
public function getDescription()
{
return 'Kunde gesperrt';
return 'Kunde deaktiviert';
}
public function getData()
{
return array(
'contact' => $this->contact
'contact' => $this->contact,
);
}

View File

@ -8,6 +8,7 @@ class UnLock extends Event
{
/** @var string */
protected $contact;
public function getType()
{
return 'contact_unlock';
@ -15,13 +16,13 @@ class UnLock extends Event
public function getDescription()
{
return 'Kunde freigeschaltet';
return 'Kunde aktiviert';
}
public function getData()
{
return array(
'contact' => $this->contact
'contact' => $this->contact,
);
}

View File

@ -9,6 +9,7 @@ class Login extends Event
{
/** @var string */
protected $contact;
public function getType()
{
return 'contact_login';
@ -16,13 +17,13 @@ class Login extends Event
public function getDescription()
{
return 'Kunde hat sich angemeldet';
return 'Kunde hat sich eingelogged';
}
public function getData()
{
return array(
'contact' => $this->contact
'contact' => $this->contact,
);
}

View File

@ -24,6 +24,7 @@ use PSC\Shop\EntityBundle\Entity\VoucherItem;
use PSC\Shop\NewsBundle\Form\Backend\DeleteType;
use PSC\Shop\PaymentBundle\Form\Backend\PaymentType;
use PSC\Shop\VoucherBundle\Form\Backend\VoucherType;
use PSC\Shop\VoucherBundle\Form\Backend\CsvUploadType;
use PSC\System\PluginBundle\Form\Chain\Field;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\File\UploadedFile;
@ -254,4 +255,265 @@ class EditController extends AbstractController
$response->headers->set('Content-Disposition', 'attachment; filename="export.csv"');
return $response;
}
/**
* Upload CSV voucher codes - Step 1: Upload and preview
*
* @param Request $request
* @param SessionInterface $session
* @param \PSC\System\SettingsBundle\Service\Shop $shopService
* @param EntityManagerInterface $entityManager
* @param $uid
* @return array|\Symfony\Component\HttpFoundation\RedirectResponse
*/
#[Route(path: '/edit/upload/{uid}', name: 'psc_shop_voucher_backend_upload')]
#[Template('@PSCShopVoucher/backend/edit/upload.html.twig')]
public function uploadAction(
Request $request,
SessionInterface $session,
\PSC\System\SettingsBundle\Service\Shop $shopService,
EntityManagerInterface $entityManager,
$uid
) {
/** @var \PSC\Shop\EntityBundle\Entity\Shop $selectedShop */
$selectedShop = $shopService->getSelectedShop();
/** @var Voucher $voucher */
$voucher = $entityManager
->getRepository('PSC\Shop\EntityBundle\Entity\Voucher')
->findOneBy(['uid' => $uid, 'shop' => $selectedShop]);
if (!$voucher) {
$session->getFlashBag()->add('error', 'Voucher not found');
return $this->redirectToRoute('psc_shop_voucher_backend_list');
}
$form = $this->createForm(CsvUploadType::class);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
/** @var UploadedFile $file */
$file = $form['file']->getData();
$result = $this->parseCsvFile($file, $voucher, $entityManager);
if ($result['success']) {
// Store codes in session for preview
$session->set('voucher_upload_preview', [
'voucher_uid' => $voucher->getUid(),
'codes' => $result['codes'],
'duplicates' => $result['duplicates']
]);
return $this->redirectToRoute('psc_shop_voucher_backend_upload_confirm', ['uid' => $uid]);
} else {
$session->getFlashBag()->add('error', $result['error']);
}
}
return [
'voucher' => $voucher,
'form' => $form->createView()
];
}
/**
* Confirm and import voucher codes - Step 2: Preview and confirm
*
* @param Request $request
* @param SessionInterface $session
* @param \PSC\System\SettingsBundle\Service\Shop $shopService
* @param EntityManagerInterface $entityManager
* @param $uid
* @return array|\Symfony\Component\HttpFoundation\RedirectResponse
*/
#[Route(path: '/edit/upload-confirm/{uid}', name: 'psc_shop_voucher_backend_upload_confirm')]
#[Template('@PSCShopVoucher/backend/edit/upload_confirm.html.twig')]
public function uploadConfirmAction(
Request $request,
SessionInterface $session,
\PSC\System\SettingsBundle\Service\Shop $shopService,
EntityManagerInterface $entityManager,
$uid
) {
/** @var \PSC\Shop\EntityBundle\Entity\Shop $selectedShop */
$selectedShop = $shopService->getSelectedShop();
/** @var Voucher $voucher */
$voucher = $entityManager
->getRepository('PSC\Shop\EntityBundle\Entity\Voucher')
->findOneBy(['uid' => $uid, 'shop' => $selectedShop]);
if (!$voucher) {
$session->getFlashBag()->add('error', 'Voucher not found');
return $this->redirectToRoute('psc_shop_voucher_backend_list');
}
$previewData = $session->get('voucher_upload_preview');
if (!$previewData || $previewData['voucher_uid'] !== $voucher->getUid()) {
$session->getFlashBag()->add('error', 'No upload data found. Please upload a CSV file first.');
return $this->redirectToRoute('psc_shop_voucher_backend_upload', ['uid' => $uid]);
}
if ($request->isMethod('POST')) {
if ($request->request->get('confirm') === 'yes') {
// Import codes
$imported = $this->importCodes($previewData['codes'], $voucher, $selectedShop, $entityManager);
// Disable "more" flag since we now have individual codes
if ($voucher->isMore()) {
$voucher->setMore(false);
$entityManager->persist($voucher);
$entityManager->flush();
}
// Clear session
$session->remove('voucher_upload_preview');
$session->getFlashBag()->add(
'success',
sprintf(
'Successfully imported %d voucher codes for "%s"',
$imported,
$voucher->getTitle()
)
);
if ($previewData['duplicates'] > 0) {
$session->getFlashBag()->add(
'warning',
sprintf(
'%d duplicate codes were skipped',
$previewData['duplicates']
)
);
}
return $this->redirectToRoute('psc_shop_voucher_backend_list');
} else {
// Cancel
$session->remove('voucher_upload_preview');
return $this->redirectToRoute('psc_shop_voucher_backend_upload', ['uid' => $uid]);
}
}
return [
'voucher' => $voucher,
'codes' => $previewData['codes'],
'duplicates' => $previewData['duplicates'],
'totalCodes' => count($previewData['codes'])
];
}
/**
* Parse CSV file and return codes
*
* @param UploadedFile $file
* @param Voucher $voucher
* @param EntityManagerInterface $entityManager
* @return array
*/
private function parseCsvFile(
UploadedFile $file,
Voucher $voucher,
EntityManagerInterface $entityManager
): array {
$codes = [];
$duplicates = 0;
$existingCodes = [];
try {
// Get all existing codes for this voucher to check for duplicates
$voucherItemRepository = $entityManager->getRepository('PSC\Shop\EntityBundle\Entity\VoucherItem');
$existingItems = $voucherItemRepository->findBy(['voucher' => $voucher]);
foreach ($existingItems as $item) {
$existingCodes[trim($item->getCode())] = true;
}
// Open and parse CSV file
if (($handle = fopen($file->getRealPath(), 'r')) !== false) {
while (($data = fgetcsv($handle, 1000, ',')) !== false) {
// Skip empty lines
if (empty($data) || !isset($data[0]) || trim($data[0]) === '') {
continue;
}
$code = trim($data[0]);
// Validate code length (max 40 chars per VoucherItem entity)
if (strlen($code) > 40) {
$code = substr($code, 0, 40);
}
// Skip if code already exists
if (isset($existingCodes[$code])) {
$duplicates++;
continue;
}
// Check if code is already in the parsed list (duplicates within file)
if (in_array($code, $codes)) {
$duplicates++;
continue;
}
$codes[] = $code;
$existingCodes[$code] = true;
}
fclose($handle);
return [
'success' => true,
'codes' => $codes,
'duplicates' => $duplicates
];
} else {
return [
'success' => false,
'error' => 'Could not open CSV file'
];
}
} catch (\Exception $e) {
return [
'success' => false,
'error' => 'Error processing CSV: ' . $e->getMessage()
];
}
}
/**
* Import codes into database
*
* @param array $codes
* @param Voucher $voucher
* @param Shop $shop
* @param EntityManagerInterface $entityManager
* @return int Number of imported codes
*/
private function importCodes(
array $codes,
Voucher $voucher,
Shop $shop,
EntityManagerInterface $entityManager
): int {
$imported = 0;
foreach ($codes as $code) {
$voucherItem = new VoucherItem();
$voucherItem->setVoucher($voucher);
$voucherItem->setShop($shop);
$voucherItem->setCode($code);
$voucherItem->setUsed(false);
$entityManager->persist($voucherItem);
$imported++;
}
$entityManager->flush();
return $imported;
}
}

View File

@ -0,0 +1,60 @@
<?php
/**
* PrintshopCreator Suite
*
* PHP Version 5.3
*
* @author Thomas Peterson <info@thomas-peterson.de>
* @copyright 2012-2013 PrintshopCreator GmbH
* @license Private
* @link http://www.printshopcreator.de
*/
namespace PSC\Shop\VoucherBundle\Form\Backend;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\FileType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Validator\Constraints\File;
/**
* CsvUploadType
*
* @package PSC\Shop\Voucher
* @subpackage Form
*/
class CsvUploadType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('file', FileType::class, [
'label' => 'csvfile',
'required' => true,
'constraints' => [
new File([
'maxSize' => '2M',
'mimeTypes' => [
'text/csv',
'text/plain',
'application/csv',
'text/comma-separated-values',
'application/vnd.ms-excel',
],
'mimeTypesMessage' => 'Please upload a valid CSV file',
])
],
])
->add('upload', SubmitType::class, ['label' => 'upload']);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'translation_domain' => 'core_voucher_upload',
]);
}
}

View File

@ -18,4 +18,5 @@ Addvoucher: Gutschein hinzufügen
Numbertogenerate: Anzahl (zu generieren)
Numbergenerated: Anzahl (generiert)
Generate: Generieren
Export: Exportieren
Export: Exportieren
Upload: Hochladen

View File

@ -18,4 +18,5 @@ Addvoucher: Add Voucher
Numbertogenerate: Number (to generate)
Numbergenerated: Number (to generated)
Generate: Generate
Export: Export
Export: Export
Upload: Upload

View File

@ -0,0 +1,18 @@
Upload: Hochladen
CSVCodes: CSV Codes
Uploadvouchercodes: Gutscheincodes hochladen
CSVUploadDescription: Laden Sie eine CSV-Datei mit Gutscheincodes hoch. Die Datei sollte eine Spalte ohne Kopfzeile enthalten, wobei jede Zeile einen Code enthält. Doppelte Codes werden übersprungen.
csvfile: CSV-Datei
upload: Hochladen
cancel: Abbrechen
back: Zurück
Preview: Vorschau
PreviewCodes: Vorschau der Codes
ReadyToImport: Codes bereit zum Import
PreviewDescription: Überprüfen Sie die Codes vor dem Import. Duplikate wurden bereits herausgefiltert.
NewCodes: Neue Codes
SkippedDuplicates: Übersprungene Duplikate
Code: Code
NoCodesToImport: Keine Codes zum Importieren gefunden.
ApplyImport: Codes übernehmen
Codes: Codes

View File

@ -0,0 +1,18 @@
Upload: Upload
CSVCodes: CSV Codes
Uploadvouchercodes: Upload voucher codes
CSVUploadDescription: Upload a CSV file containing voucher codes. The file should contain one column without a header, with each row containing one code. Duplicate codes will be skipped.
csvfile: CSV File
upload: Upload
cancel: Cancel
back: Back
Preview: Preview
PreviewCodes: Code Preview
ReadyToImport: Codes ready to import
PreviewDescription: Review the codes before importing. Duplicates have already been filtered out.
NewCodes: New codes
SkippedDuplicates: Skipped duplicates
Code: Code
NoCodesToImport: No codes found to import.
ApplyImport: Apply Import
Codes: Codes

View File

@ -13,7 +13,7 @@
</h1>
</div>
<div class="flex flex-wrap items-center gap-4 justify-end shrink-0 ml-auto">
<a href="{{ path('psc_shop_voucher_backend_list') }}" class="inline-flex items-center justify-center py-1 gap-1 font-medium rounded-md px-4 text-sm text-white shadow-lg bg-psc-500 hover:bg-psc-600 hover:ring-2 hover:ring-psc-500 hover:ring-offset-2 min-h-[2.25rem]">
<a href="{{ path('psc_shop_voucher_backend_list') }}" class="inline-flex items-center justify-center py-1 gap-1 font-medium rounded-md px-4 text-sm text-white shadow-lg bg-gray-500 hover:bg-gray-600 hover:ring-2 hover:ring-gray-500 hover:ring-offset-2 min-h-[2.25rem]">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="button-icon">
<path stroke-linecap="round" stroke-linejoin="round" d="M10.5 19.5L3 12m0 0l7.5-7.5M3 12h18" />
</svg>

View File

@ -13,7 +13,7 @@
</h1>
</div>
<div class="flex flex-wrap items-center gap-4 justify-end shrink-0 ml-auto">
<a href="{{ path('psc_shop_voucher_backend_list') }}" class="inline-flex items-center justify-center py-1 gap-1 font-medium rounded-md px-4 text-sm text-white shadow-lg bg-psc-500 hover:bg-psc-600 hover:ring-2 hover:ring-psc-500 hover:ring-offset-2 min-h-[2.25rem]">
<a href="{{ path('psc_shop_voucher_backend_list') }}" class="inline-flex items-center justify-center py-1 gap-1 font-medium rounded-md px-4 text-sm text-white shadow-lg bg-gray-500 hover:bg-gray-600 hover:ring-2 hover:ring-gray-500 hover:ring-offset-2 min-h-[2.25rem]">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="button-icon">
<path stroke-linecap="round" stroke-linejoin="round" d="M10.5 19.5L3 12m0 0l7.5-7.5M3 12h18" />
</svg>

View File

@ -0,0 +1,54 @@
{% extends 'backend_tailwind_base.html.twig' %}
{% form_theme form 'tailwind_formtheme.html.twig' %}
{% trans_default_domain 'core_voucher_upload' %}
{% block header %}
<div class="flex flex-wrap items-center gap-4 justify-between w-full">
<div>
<h1 class="text-psc text-2xl font-medium flex flex-row gap-1">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="h-8">
<path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5m-13.5-9L12 3m0 0l4.5 4.5M12 3v13.5" />
</svg>
{{'Upload'|trans}} <span class="text-gray-500">{{'CSVCodes'|trans}}</span>
</h1>
</div>
<div class="flex flex-wrap items-center gap-4 justify-end shrink-0 ml-auto">
<a href="{{ path('psc_shop_voucher_backend_list') }}" class="inline-flex items-center justify-center py-1 gap-1 font-medium rounded-md px-4 text-sm text-white shadow-lg bg-gray-500 hover:bg-gray-600 hover:ring-2 hover:ring-gray-500 hover:ring-offset-2 min-h-[2.25rem]">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="button-icon">
<path stroke-linecap="round" stroke-linejoin="round" d="M10.5 19.5L3 12m0 0l7.5-7.5M3 12h18" />
</svg>
{{'back'|trans}}
</a>
</div>
</div>
{% endblock %}
{% block body %}
<div class="w-full flex flex-col gap-6">
{{ form_start(form, {attr: {class: '', enctype: 'multipart/form-data'}}) }}
<div class="rounded-md w-full border bg-white p-5 shadow-lg dark:border-strokedark dark:bg-boxdark">
<h6 class="text-sm mt-3 mb-6 font-bold uppercase">{{'Uploadvouchercodes'|trans}}: {{ voucher.title }}</h6>
<p class="mb-6 text-sm text-gray-600 dark:text-gray-400">
{{'CSVUploadDescription'|trans}}
</p>
<div class="flex flex-wrap">
<div class="w-full lg:w-6/12 px-4">
{{ form_row(form.file) }}
</div>
</div>
</div>
<div class="text-end my-2">
{{ form_widget(form.upload, {
attr: {
class: 'inline-flex items-center justify-center py-1 gap-1 font-medium rounded-md px-4 text-sm text-white shadow-lg bg-psc-500 hover:bg-psc-600 hover:ring-2 hover:ring-psc-500 hover:ring-offset-2 min-h-[2.25rem]'
}
}) }}
</div>
{{ form_end(form) }}
</div>
{% endblock %}

View File

@ -0,0 +1,102 @@
{% extends 'backend_tailwind_base.html.twig' %}
{% trans_default_domain 'core_voucher_upload' %}
{% block header %}
<div class="flex flex-wrap items-center gap-4 justify-between w-full">
<div>
<h1 class="text-psc text-2xl font-medium flex flex-row gap-1">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="h-8">
<path stroke-linecap="round" stroke-linejoin="round" d="M2.036 12.322a1.012 1.012 0 010-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178z" />
<path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
{{'Preview'|trans}} <span class="text-gray-500">{{'CSVCodes'|trans}}</span>
</h1>
</div>
<div class="flex flex-wrap items-center gap-4 justify-end shrink-0 ml-auto">
<a href="{{ path('psc_shop_voucher_backend_upload', {uid: voucher.uid}) }}" class="inline-flex items-center justify-center py-1 gap-1 font-medium rounded-md px-4 text-sm text-white shadow-lg bg-gray-500 hover:bg-gray-600 hover:ring-2 hover:ring-gray-500 hover:ring-offset-2 min-h-[2.25rem]">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="button-icon">
<path stroke-linecap="round" stroke-linejoin="round" d="M10.5 19.5L3 12m0 0l7.5-7.5M3 12h18" />
</svg>
{{'back'|trans}}
</a>
</div>
</div>
{% endblock %}
{% block body %}
<div class="w-full flex flex-col gap-6">
<div class="rounded-md w-full border bg-white p-5 shadow-lg dark:border-strokedark dark:bg-boxdark">
<h6 class="text-sm mt-3 mb-6 font-bold uppercase">{{'PreviewCodes'|trans}}: {{ voucher.title }}</h6>
<div class="mb-6 p-4 bg-blue-50 dark:bg-blue-900/20 rounded-md border border-blue-200 dark:border-blue-800">
<div class="flex items-start gap-3">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5 text-blue-600 dark:text-blue-400 mt-0.5 flex-shrink-0">
<path stroke-linecap="round" stroke-linejoin="round" d="M11.25 11.25l.041-.02a.75.75 0 011.063.852l-.708 2.836a.75.75 0 001.063.853l.041-.021M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9-3.75h.008v.008H12V8.25z" />
</svg>
<div class="text-sm text-blue-800 dark:text-blue-200">
<p class="font-semibold mb-1">{{'ReadyToImport'|trans}}</p>
<p>{{'PreviewDescription'|trans}}</p>
<ul class="mt-2 space-y-1">
<li><strong>{{'NewCodes'|trans}}:</strong> {{ totalCodes }}</li>
{% if duplicates > 0 %}
<li><strong>{{'SkippedDuplicates'|trans}}:</strong> {{ duplicates }}</li>
{% endif %}
</ul>
</div>
</div>
</div>
<div class="overflow-x-auto">
<table class="min-w-full text-sm">
<thead class="bg-slate-50 dark:bg-gray-800">
<tr class="border-b-2 border-gray-200 dark:border-gray-700">
<th class="px-4 py-4 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider dark:text-gray-300">
#
</th>
<th class="px-4 py-4 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider dark:text-gray-300">
{{'Code'|trans}}
</th>
</tr>
</thead>
<tbody class="bg-white dark:bg-boxdark divide-y divide-gray-100 dark:divide-gray-700">
{% for code in codes %}
<tr class="hover:bg-gray-50 dark:hover:bg-gray-800/50 transition-colors">
<td class="px-4 py-3 text-gray-500 dark:text-gray-400">
{{ loop.index }}
</td>
<td class="px-4 py-3 font-mono text-gray-900 dark:text-gray-100">
{{ code }}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% if totalCodes == 0 %}
<div class="mt-4 p-4 bg-yellow-50 dark:bg-yellow-900/20 rounded-md border border-yellow-200 dark:border-yellow-800">
<p class="text-sm text-yellow-800 dark:text-yellow-200">
{{'NoCodesToImport'|trans}}
</p>
</div>
{% endif %}
</div>
<form method="post">
<div class="flex gap-3 justify-end">
<button type="submit" name="confirm" value="no" class="inline-flex items-center justify-center py-2 px-5 text-sm font-medium rounded-md text-gray-700 bg-white border border-gray-300 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500 dark:bg-gray-700 dark:text-gray-300 dark:border-gray-600 dark:hover:bg-gray-600">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4 mr-2">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
{{'cancel'|trans}}
</button>
<button type="submit" name="confirm" value="yes" class="inline-flex items-center justify-center py-2 px-5 text-sm font-medium rounded-md text-white shadow-lg bg-psc-500 hover:bg-psc-600 hover:ring-2 hover:ring-psc-500 hover:ring-offset-2 focus:outline-none focus:ring-2 focus:ring-psc-500" {% if totalCodes == 0 %}disabled{% endif %}>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4 mr-2">
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
{{'ApplyImport'|trans}} ({{ totalCodes }} {{'Codes'|trans}})
</button>
</div>
</form>
</div>
{% endblock %}

View File

@ -103,6 +103,12 @@
<td class="px-4 py-3 text-gray-900 dark:text-gray-100">{% if voucher.more %}Keine Individuellen Codes{% else %}{{ voucher.voucherItems|length }}{% endif %}</td>
<td class="px-4 py-3 text-right">
<div class="flex flex-row gap-2 justify-end">
{# Upload - LILA #}
<a href="{{ path('psc_shop_voucher_backend_upload', {uid: voucher.uid}) }}" title="{{'Upload'|trans}}">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="table-icon text-purple-600 hover:text-purple-700 dark:text-purple-500 dark:hover:text-purple-400">
<path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5m-13.5-9L12 3m0 0l4.5 4.5M12 3v13.5" />
</svg>
</a>
{# Export - BLAU #}
<a href="{{ path('psc_shop_voucher_backend_export', {uid: voucher.uid}) }}" target="_blank" title="{{'Export'|trans}}">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="table-icon text-blue-600 hover:text-blue-700 dark:text-blue-500 dark:hover:text-blue-400">

View File

@ -58,6 +58,8 @@
</svg>
<h5 class="text-lg font-semibold mb-2">PDF-Dateien hier ablegen</h5>
<p class="text-gray-500">oder klicken zum Auswählen</p>
<p class="text-gray-400 text-sm mt-2">Mehrseitige PDFs werden automatisch in 2-seitige PDFs aufgeteilt</p>
<p class="text-gray-400 text-xs">(Nur PDFs mit gerader Seitenzahl)</p>
<input type="file" id="pdfInput" accept=".pdf" multiple class="hidden">
</div>
<div id="uploadedFilesList" class="mt-4"></div>
@ -211,6 +213,7 @@
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/tesseract.js@4/dist/tesseract.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/pdf-lib@1.17.1/dist/pdf-lib.min.js"></script>
<script>
// Global variables
let uploadedFiles = [];
@ -942,12 +945,123 @@
handleFiles(e.target.files);
});
function handleFiles(files) {
// Append new files instead of replacing
const newFiles = Array.from(files).filter(file => file.type === 'application/pdf');
uploadedFiles = [...uploadedFiles, ...newFiles];
/**
* Split a multi-page PDF into 2-page chunks
* @param {File} file - The PDF file to split
* @returns {Promise<Array>} Array of {file: File, error: string|null}
*/
async function splitPdfIntoChunks(file) {
try {
// Read the PDF file
const arrayBuffer = await file.arrayBuffer();
const pdfDoc = await PDFLib.PDFDocument.load(arrayBuffer);
const totalPages = pdfDoc.getPageCount();
// If 1-2 pages, no splitting needed
if (totalPages <= 2) {
return [{ file: file, error: null }];
}
// Check if page count is even
if (totalPages % 2 !== 0) {
return [{
file: null,
error: `${file.name} hat ${totalPages} Seiten (ungerade). Nur PDFs mit gerader Seitenzahl können gesplittet werden.`
}];
}
console.log(`Splitting ${file.name} (${totalPages} pages) into ${totalPages / 2} 2-page PDFs...`);
const splitPdfs = [];
const baseName = file.name.replace('.pdf', '');
// Split into 2-page chunks
for (let i = 0; i < totalPages; i += 2) {
const newPdf = await PDFLib.PDFDocument.create();
// Copy 2 pages
const [page1, page2] = await newPdf.copyPages(pdfDoc, [i, i + 1]);
newPdf.addPage(page1);
newPdf.addPage(page2);
// Save as new PDF
const pdfBytes = await newPdf.save();
const blob = new Blob([pdfBytes], { type: 'application/pdf' });
const chunkNumber = (i / 2) + 1;
const totalChunks = totalPages / 2;
const newFileName = `${baseName}_Teil_${chunkNumber}_von_${totalChunks}.pdf`;
const newFile = new File([blob], newFileName, { type: 'application/pdf' });
splitPdfs.push({ file: newFile, error: null });
}
console.log(`Successfully split ${file.name} into ${splitPdfs.length} files`);
return splitPdfs;
} catch (error) {
console.error(`Error splitting PDF ${file.name}:`, error);
return [{
file: null,
error: `Fehler beim Splitten von ${file.name}: ${error.message}`
}];
}
}
async function handleFiles(files) {
const pdfFiles = Array.from(files).filter(file => file.type === 'application/pdf');
if (pdfFiles.length === 0) {
return;
}
// Show progress container
const progressContainer = document.getElementById('progress-container');
const progressBar = document.getElementById('upload-progress');
progressContainer.classList.remove('hidden');
progressBar.style.width = '0%';
progressBar.textContent = '0%';
const errors = [];
const processedFiles = [];
// Process each PDF file (split if needed)
for (let i = 0; i < pdfFiles.length; i++) {
const file = pdfFiles[i];
const progress = Math.round(((i + 1) / pdfFiles.length) * 100);
progressBar.style.width = `${progress}%`;
progressBar.textContent = `${progress}%`;
// Split PDF if it has more than 2 pages
const splitResults = await splitPdfIntoChunks(file);
// Collect results
splitResults.forEach(result => {
if (result.error) {
errors.push(result.error);
} else if (result.file) {
processedFiles.push(result.file);
}
});
}
// Hide progress
progressContainer.classList.add('hidden');
// Show errors if any
if (errors.length > 0) {
alert('Fehler beim Verarbeiten:\n\n' + errors.join('\n'));
}
// Add processed files to uploadedFiles
uploadedFiles = [...uploadedFiles, ...processedFiles];
displayUploadedFiles();
document.getElementById('nextToStep2').disabled = uploadedFiles.length === 0;
// Show success message if files were split
const splitCount = processedFiles.length - pdfFiles.filter(f => processedFiles.some(pf => pf.name === f.name)).length;
if (splitCount > 0) {
const originalCount = pdfFiles.length - errors.length;
alert(`${originalCount} mehrseitige PDF(s) wurden erfolgreich in ${processedFiles.length} 2-seitige PDFs aufgeteilt!`);
}
}
function removeFile(index) {
@ -1053,6 +1167,316 @@
});
// OCR functionality
/**
* Perform OCR on a specific area of the canvas
* @param {HTMLCanvasElement} canvas - The source canvas
* @param {number} x - X coordinate
* @param {number} y - Y coordinate
* @param {number} sourceWidth - Width of the area
* @param {number} sourceHeight - Height of the area
* @returns {Promise<Object>} { extractedNumber: string, previewDataUrl: string, tempCanvas: HTMLCanvasElement }
*/
async function performOCRAtPosition(canvas, x, y, sourceWidth, sourceHeight) {
// Create a temporary canvas for the OCR area
const tempCanvas = document.createElement('canvas');
const scale = 4; // Scale up for better OCR
tempCanvas.width = sourceWidth * scale;
tempCanvas.height = sourceHeight * scale;
const tempCtx = tempCanvas.getContext('2d');
// Fill with white background first
tempCtx.fillStyle = 'white';
tempCtx.fillRect(0, 0, tempCanvas.width, tempCanvas.height);
// Copy and scale up the specified area from the main canvas
tempCtx.drawImage(canvas, x, y, sourceWidth, sourceHeight, 0, 0, tempCanvas.width, tempCanvas.height);
// Image preprocessing for better OCR
const imageData = tempCtx.getImageData(0, 0, tempCanvas.width, tempCanvas.height);
const data = imageData.data;
// Increase contrast and convert to grayscale
for (let i = 0; i < data.length; i += 4) {
// Convert to grayscale
const gray = data[i] * 0.299 + data[i + 1] * 0.587 + data[i + 2] * 0.114;
// Increase contrast (threshold binarization)
const threshold = 128;
const binary = gray > threshold ? 255 : 0;
data[i] = binary; // R
data[i + 1] = binary; // G
data[i + 2] = binary; // B
}
tempCtx.putImageData(imageData, 0, 0);
// Save OCR preview for table (downscaled version)
const previewCanvas = document.createElement('canvas');
previewCanvas.width = sourceWidth;
previewCanvas.height = sourceHeight;
const previewCtx = previewCanvas.getContext('2d');
previewCtx.drawImage(tempCanvas, 0, 0, sourceWidth, sourceHeight);
const previewDataUrl = previewCanvas.toDataURL();
console.log('Scanning at position:', { x, y, sourceWidth, sourceHeight });
const result = await Tesseract.recognize(
tempCanvas,
'eng',
{
logger: m => {
if (m.status === 'recognizing text') {
console.log(`Fortschritt: ${(m.progress * 100).toFixed(0)}%`);
}
},
tessedit_char_whitelist: '0123456789',
tessedit_pageseg_mode: Tesseract.PSM.SINGLE_LINE
}
);
// Extract only numbers from the recognized text
console.log('OCR Result:', result);
console.log('Erkannter Text:', result.data.text);
const numberMatch = result.data.text.match(/\d+/);
const extractedNumber = numberMatch ? numberMatch[0] : '';
return {
extractedNumber,
previewDataUrl,
tempCanvas,
confidence: result.data.confidence
};
}
/**
* Scan specifically the top-right corner where MELDEGRUPPE typically appears
*/
async function scanTopRightCorner(canvas) {
console.log('Scanning top-right corner for MELDEGRUPPE...');
// Scan the top-right corner (where MELDEGRUPPE is in the screenshot)
// Make it larger to ensure we capture the number
const scanWidth = Math.min(canvas.width * 0.3, 400); // Right 30% of page, max 400px (increased!)
const scanHeight = Math.min(canvas.height * 0.2, 300); // Top 20%, max 300px (increased!)
const scanX = canvas.width - scanWidth; // Start from right
const scanY = 0; // Top of page
// Create a temporary canvas for the OCR area
const tempCanvas = document.createElement('canvas');
const scale = 2; // Lower scale for larger area (better performance)
tempCanvas.width = scanWidth * scale;
tempCanvas.height = scanHeight * scale;
const tempCtx = tempCanvas.getContext('2d');
// Fill with white background first
tempCtx.fillStyle = 'white';
tempCtx.fillRect(0, 0, tempCanvas.width, tempCanvas.height);
// Copy and scale up the specified area from the main canvas
tempCtx.drawImage(canvas, scanX, scanY, scanWidth, scanHeight, 0, 0, tempCanvas.width, tempCanvas.height);
// IMPROVED: Gentler image preprocessing for better OCR
const imageData = tempCtx.getImageData(0, 0, tempCanvas.width, tempCanvas.height);
const data = imageData.data;
// Convert to grayscale with adaptive thresholding
// First pass: calculate average brightness
let totalBrightness = 0;
for (let i = 0; i < data.length; i += 4) {
const gray = data[i] * 0.299 + data[i + 1] * 0.587 + data[i + 2] * 0.114;
totalBrightness += gray;
}
const avgBrightness = totalBrightness / (data.length / 4);
// Use adaptive threshold based on average brightness
const threshold = avgBrightness * 0.7; // 70% of average
// Second pass: apply threshold
for (let i = 0; i < data.length; i += 4) {
const gray = data[i] * 0.299 + data[i + 1] * 0.587 + data[i + 2] * 0.114;
// Use softer threshold for better number recognition
const binary = gray > threshold ? 255 : 0;
data[i] = binary;
data[i + 1] = binary;
data[i + 2] = binary;
}
tempCtx.putImageData(imageData, 0, 0);
console.log('Scanning top-right corner:', { scanX, scanY, scanWidth, scanHeight });
// Perform OCR on the top-right corner with optimized settings
const result = await Tesseract.recognize(
tempCanvas,
'deu+eng', // Use both German and English for better recognition
{
logger: m => {
if (m.status === 'recognizing text') {
console.log(`Fortschritt (top-right): ${(m.progress * 100).toFixed(0)}%`);
}
},
// Optimize for mixed text and numbers
tessedit_pageseg_mode: Tesseract.PSM.AUTO,
}
);
console.log('Top-right corner OCR Text:', result.data.text);
const text = result.data.text;
// Very flexible patterns to handle OCR errors
const patterns = [
/MELDE[RN]?GRUPPE[:\s]*[\r\n]*\s*(\d+)/i, // MELDEGRUPPE or MELDERGRUPPE or MELDENGRUPPE
/MELDER[:\s]*[\r\n]*\s*(\d+)/i, // Just MELDER (partial recognition)
/GRUPPE[:\s]*[\r\n]*\s*(\d+)/i, // Just GRUPPE
/M[EL]+D[EL]*[RN]?GR[UO]+PP[EL]*[:\s]*[\r\n]*\s*(\d+)/i, // Very flexible MELDEGRUPPE variant
];
let extractedNumber = '';
for (const pattern of patterns) {
const match = text.match(pattern);
if (match && match[1]) {
extractedNumber = match[1];
console.log(`Found "Meldegruppe" in top-right with pattern ${pattern}:`, extractedNumber);
break;
}
}
// Primary Fallback: look for ANY number (1-5 digits) in top-right
// Since this is the MELDEGRUPPE area, any number here is likely the group number
if (!extractedNumber) {
const numberMatches = text.match(/\d+/g);
if (numberMatches && numberMatches.length > 0) {
// Find the largest number (most likely the group number)
extractedNumber = numberMatches.reduce((max, num) => num.length > max.length ? num : max, '');
console.log('Found number in top-right (fallback):', extractedNumber);
}
}
const previewCanvas = document.createElement('canvas');
previewCanvas.width = 200;
previewCanvas.height = 100;
const previewCtx = previewCanvas.getContext('2d');
previewCtx.drawImage(tempCanvas, 0, 0, 200, 100);
const previewDataUrl = previewCanvas.toDataURL();
return {
extractedNumber,
previewDataUrl,
tempCanvas,
confidence: result.data.confidence,
fullText: text
};
}
/**
* Advanced OCR that scans a larger area and searches for "Meldegruppe" pattern
*/
async function performAdvancedOCR(canvas) {
console.log('Performing advanced OCR (searching for "Meldegruppe")...');
// Scan the upper third of the page
const scanWidth = Math.min(canvas.width, 800); // Limit width for performance
const scanHeight = Math.min(canvas.height / 3, 400); // Upper third, max 400px
const scanX = 0;
const scanY = 0;
// Create a temporary canvas for the OCR area
const tempCanvas = document.createElement('canvas');
const scale = 2; // Scale for better OCR (lower than before for larger area)
tempCanvas.width = scanWidth * scale;
tempCanvas.height = scanHeight * scale;
const tempCtx = tempCanvas.getContext('2d');
// Fill with white background first
tempCtx.fillStyle = 'white';
tempCtx.fillRect(0, 0, tempCanvas.width, tempCanvas.height);
// Copy and scale up the specified area from the main canvas
tempCtx.drawImage(canvas, scanX, scanY, scanWidth, scanHeight, 0, 0, tempCanvas.width, tempCanvas.height);
// Image preprocessing for better OCR
const imageData = tempCtx.getImageData(0, 0, tempCanvas.width, tempCanvas.height);
const data = imageData.data;
// Increase contrast and convert to grayscale
for (let i = 0; i < data.length; i += 4) {
const gray = data[i] * 0.299 + data[i + 1] * 0.587 + data[i + 2] * 0.114;
const threshold = 128;
const binary = gray > threshold ? 255 : 0;
data[i] = binary;
data[i + 1] = binary;
data[i + 2] = binary;
}
tempCtx.putImageData(imageData, 0, 0);
console.log('Scanning large area:', { scanWidth, scanHeight });
// Perform OCR on the larger area
const result = await Tesseract.recognize(
tempCanvas,
'deu', // German language for better "Meldegruppe" recognition
{
logger: m => {
if (m.status === 'recognizing text') {
console.log(`Fortschritt: ${(m.progress * 100).toFixed(0)}%`);
}
}
}
);
console.log('Full OCR Text:', result.data.text);
// Search for "Meldegruppe" pattern and extract number
const text = result.data.text;
// Multiple patterns to handle different formats
const patterns = [
/MELDEGRUPPE[:\s]*[\r\n]*\s*(\d+)/i, // MELDEGRUPPE: \n10000
/Meldegruppe[:\s]*[\r\n]*\s*(\d+)/i, // Meldegruppe: \n12
/Melde[\s-]*gruppe[:\s]*[\r\n]*\s*(\d+)/i, // Melde-gruppe or Melde gruppe
/MG[:\s]*[\r\n]*\s*(\d+)/i, // MG: 12
/GRUPPE[:\s]*[\r\n]*\s*(\d+)/i, // Just GRUPPE (in case OCR misses MELDE)
];
let extractedNumber = '';
// Try each pattern
for (const pattern of patterns) {
const match = text.match(pattern);
if (match && match[1]) {
extractedNumber = match[1];
console.log(`Found "Meldegruppe" with pattern ${pattern}:`, extractedNumber);
break;
}
}
// If no pattern matched, try to find standalone large numbers (likely the Meldegruppe)
// Look for 4-5 digit numbers that might be the group number
if (!extractedNumber) {
const standaloneNumberMatch = text.match(/\b(\d{4,5})\b/);
if (standaloneNumberMatch) {
extractedNumber = standaloneNumberMatch[1];
console.log('Found standalone number (fallback):', extractedNumber);
}
}
// Create a small preview canvas showing where we found the text
const previewCanvas = document.createElement('canvas');
previewCanvas.width = 200;
previewCanvas.height = 100;
const previewCtx = previewCanvas.getContext('2d');
previewCtx.drawImage(tempCanvas, 0, 0, 200, 100);
const previewDataUrl = previewCanvas.toDataURL();
return {
extractedNumber,
previewDataUrl,
tempCanvas,
confidence: result.data.confidence,
fullText: text
};
}
async function processOCR(fileIndex) {
const progressDiv = document.getElementById('ocr-progress');
@ -1073,88 +1497,85 @@
console.log('Canvas before OCR:', canvas.width, 'x', canvas.height);
console.log('Canvas has content:', canvas.toDataURL().length > 5000);
// Create a temporary canvas for the OCR area
const tempCanvas = document.createElement('canvas');
const sourceWidth = 150;
const sourceHeight = 50;
const scale = 4; // Scale up for better OCR
tempCanvas.width = sourceWidth * scale;
tempCanvas.height = sourceHeight * scale;
const tempCtx = tempCanvas.getContext('2d');
let finalResult = null;
let usedMethod = '';
// Fill with white background first
tempCtx.fillStyle = 'white';
tempCtx.fillRect(0, 0, tempCanvas.width, tempCanvas.height);
// Strategy 1: Try top-right corner first (most common location for MELDEGRUPPE)
console.log('Strategy 1: Scanning top-right corner...');
const topRightResult = await scanTopRightCorner(canvas);
// Copy and scale up the specified area from the main canvas
tempCtx.drawImage(canvas, 20, 50, sourceWidth, sourceHeight, 0, 0, tempCanvas.width, tempCanvas.height);
if (topRightResult.extractedNumber && topRightResult.extractedNumber.trim() !== '') {
finalResult = topRightResult;
usedMethod = 'Oben-Rechts (MELDEGRUPPE)';
console.log('Success with top-right scan:', topRightResult.extractedNumber);
} else {
// Strategy 2: Try advanced OCR (search for "Meldegruppe" in larger area)
console.log('Strategy 2: Advanced OCR (searching for "Meldegruppe" in full upper area)...');
const advancedResult = await performAdvancedOCR(canvas);
// Image preprocessing for better OCR
const imageData = tempCtx.getImageData(0, 0, tempCanvas.width, tempCanvas.height);
const data = imageData.data;
if (advancedResult.extractedNumber && advancedResult.extractedNumber.trim() !== '') {
finalResult = advancedResult;
usedMethod = 'Erweitert (Meldegruppe gefunden)';
console.log('Success with advanced OCR:', advancedResult.extractedNumber);
} else {
// Strategy 3: Fallback to position-based scanning (left/right)
console.log('Strategy 3: Fallback to position-based scanning...');
// Increase contrast and convert to grayscale
for (let i = 0; i < data.length; i += 4) {
// Convert to grayscale
const gray = data[i] * 0.299 + data[i + 1] * 0.587 + data[i + 2] * 0.114;
const sourceWidth = 150;
const sourceHeight = 50;
const sourceY = 50;
// Increase contrast (threshold binarization)
const threshold = 128;
const binary = gray > threshold ? 255 : 0;
// Try left position
const leftX = 20;
console.log('Trying LEFT position...');
const leftResult = await performOCRAtPosition(canvas, leftX, sourceY, sourceWidth, sourceHeight);
data[i] = binary; // R
data[i + 1] = binary; // G
data[i + 2] = binary; // B
if (leftResult.extractedNumber && leftResult.extractedNumber.trim() !== '') {
finalResult = leftResult;
usedMethod = 'Position-basiert (links)';
console.log('Found result on LEFT:', leftResult.extractedNumber);
} else {
// Try right position
console.log('No result on LEFT, trying RIGHT position...');
const rightX = canvas.width - sourceWidth - 20;
const rightResult = await performOCRAtPosition(canvas, rightX, sourceY, sourceWidth, sourceHeight);
if (rightResult.extractedNumber && rightResult.extractedNumber.trim() !== '') {
finalResult = rightResult;
usedMethod = 'Position-basiert (rechts)';
console.log('Found result on RIGHT:', rightResult.extractedNumber);
} else {
// No result found anywhere
finalResult = rightResult; // Use empty result
usedMethod = 'Keine Nummer gefunden';
console.log('No result found with any method');
}
}
}
}
tempCtx.putImageData(imageData, 0, 0);
// Display in debug canvas
const debugCanvas = document.getElementById('debugCanvas');
debugCanvas.width = tempCanvas.width;
debugCanvas.height = tempCanvas.height;
const debugCtx = debugCanvas.getContext('2d');
debugCtx.drawImage(tempCanvas, 0, 0);
document.getElementById('debugCanvasSize').textContent = `${tempCanvas.width}x${tempCanvas.height}px`;
if (finalResult) {
const debugCanvas = document.getElementById('debugCanvas');
debugCanvas.width = finalResult.tempCanvas.width;
debugCanvas.height = finalResult.tempCanvas.height;
const debugCtx = debugCanvas.getContext('2d');
debugCtx.drawImage(finalResult.tempCanvas, 0, 0);
document.getElementById('debugCanvasSize').textContent = `${finalResult.tempCanvas.width}x${finalResult.tempCanvas.height}px (${usedMethod})`;
// Save OCR preview for table (downscaled version)
const previewCanvas = document.createElement('canvas');
previewCanvas.width = sourceWidth;
previewCanvas.height = sourceHeight;
const previewCtx = previewCanvas.getContext('2d');
previewCtx.drawImage(tempCanvas, 0, 0, sourceWidth, sourceHeight);
ocrPreviews[fileIndex] = previewCanvas.toDataURL();
// Save OCR preview for table
ocrPreviews[fileIndex] = finalResult.previewDataUrl;
console.log('Temp canvas data length:', tempCanvas.toDataURL().length);
// Update the PDF data
pdfData[fileIndex].ocrNumber = finalResult.extractedNumber;
pdfData[fileIndex].processed = true;
const result = await Tesseract.recognize(
tempCanvas,
'eng',
{
logger: m => {
if (m.status === 'recognizing text') {
console.log(`Fortschritt: ${(m.progress * 100).toFixed(0)}%`);
}
},
tessedit_char_whitelist: '0123456789',
tessedit_pageseg_mode: Tesseract.PSM.SINGLE_LINE
}
);
// Update the input field
document.getElementById(`ocr-input-${fileIndex}`).value = finalResult.extractedNumber;
// Extract only numbers from the recognized text
console.log('OCR Result:', result);
console.log('Erkannter Text:', result.data.text);
const numberMatch = result.data.text.match(/\d+/);
const extractedNumber = numberMatch ? numberMatch[0] : '';
// Update the PDF data
pdfData[fileIndex].ocrNumber = extractedNumber;
pdfData[fileIndex].processed = true;
// Update the input field
document.getElementById(`ocr-input-${fileIndex}`).value = extractedNumber;
// Update the table
updatePDFTable();
// Update the table
updatePDFTable();
}
// Close modal
document.getElementById('previewModal').classList.add('hidden');

View File

@ -2219,6 +2219,11 @@ html {
color: rgb(172 48 37 / var(--tw-text-opacity));
}
.text-purple-600{
--tw-text-opacity: 1;
color: rgb(147 51 234 / var(--tw-text-opacity));
}
.text-red-600{
--tw-text-opacity: 1;
color: rgb(220 38 38 / var(--tw-text-opacity));
@ -2827,6 +2832,11 @@ html {
color: rgb(144 40 31 / var(--tw-text-opacity));
}
.hover\:text-purple-700:hover{
--tw-text-opacity: 1;
color: rgb(126 34 206 / var(--tw-text-opacity));
}
.hover\:text-red-700:hover{
--tw-text-opacity: 1;
color: rgb(185 28 28 / var(--tw-text-opacity));
@ -2973,6 +2983,11 @@ html {
--tw-ring-color: rgb(229 231 235 / var(--tw-ring-opacity));
}
.focus\:ring-gray-500:focus{
--tw-ring-opacity: 1;
--tw-ring-color: rgb(107 114 128 / var(--tw-ring-opacity));
}
.focus\:ring-psc-500:focus{
--tw-ring-opacity: 1;
--tw-ring-color: rgb(234 100 27 / var(--tw-ring-opacity));
@ -2987,6 +3002,10 @@ html {
--tw-ring-opacity: 0.5;
}
.focus\:ring-offset-2:focus{
--tw-ring-offset-width: 2px;
}
.disabled\:cursor-not-allowed:disabled{
cursor: not-allowed;
}
@ -3186,6 +3205,11 @@ html {
color: rgb(34 197 94 / var(--tw-text-opacity));
}
:is(.dark .dark\:text-purple-500){
--tw-text-opacity: 1;
color: rgb(168 85 247 / var(--tw-text-opacity));
}
:is(.dark .dark\:text-red-200){
--tw-text-opacity: 1;
color: rgb(254 202 202 / var(--tw-text-opacity));
@ -3288,6 +3312,11 @@ html {
color: rgb(220 102 91 / var(--tw-text-opacity));
}
:is(.dark .dark\:hover\:text-purple-400:hover){
--tw-text-opacity: 1;
color: rgb(192 132 252 / var(--tw-text-opacity));
}
:is(.dark .dark\:hover\:text-red-400:hover){
--tw-text-opacity: 1;
color: rgb(248 113 113 / var(--tw-text-opacity));
@ -3433,10 +3462,6 @@ html {
width: 41.666667%;
}
.md\:w-9\/12{
width: 75%;
}
.md\:w-fit{
width: -moz-fit-content;
width: fit-content;
@ -3514,10 +3539,6 @@ html {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.lg\:grid-cols-3{
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.lg\:gap-8{
gap: 2rem;
}