Fixes
This commit is contained in:
parent
7e46c1fc5d
commit
3a0019a1b9
@ -474,7 +474,7 @@ namespace Symfony\Component\DependencyInjection\Loader\Configurator;
|
|||||||
* datetime?: array{
|
* datetime?: array{
|
||||||
* default_format?: scalar|null, // Default: "Y-m-d\\TH:i:sP"
|
* default_format?: scalar|null, // Default: "Y-m-d\\TH:i:sP"
|
||||||
* default_deserialization_formats?: list<scalar|null>,
|
* default_deserialization_formats?: list<scalar|null>,
|
||||||
* default_timezone?: scalar|null, // Default: "UTC"
|
* default_timezone?: scalar|null, // Default: "Europe/Berlin"
|
||||||
* cdata?: scalar|null, // Default: true
|
* cdata?: scalar|null, // Default: true
|
||||||
* },
|
* },
|
||||||
* array_collection?: array{
|
* array_collection?: array{
|
||||||
@ -574,7 +574,7 @@ namespace Symfony\Component\DependencyInjection\Loader\Configurator;
|
|||||||
* datetime?: array{
|
* datetime?: array{
|
||||||
* default_format?: scalar|null, // Default: "Y-m-d\\TH:i:sP"
|
* default_format?: scalar|null, // Default: "Y-m-d\\TH:i:sP"
|
||||||
* default_deserialization_formats?: list<scalar|null>,
|
* default_deserialization_formats?: list<scalar|null>,
|
||||||
* default_timezone?: scalar|null, // Default: "UTC"
|
* default_timezone?: scalar|null, // Default: "Europe/Berlin"
|
||||||
* cdata?: scalar|null, // Default: true
|
* cdata?: scalar|null, // Default: true
|
||||||
* },
|
* },
|
||||||
* array_collection?: array{
|
* array_collection?: array{
|
||||||
|
|||||||
@ -8,6 +8,7 @@ class Lock extends Event
|
|||||||
{
|
{
|
||||||
/** @var string */
|
/** @var string */
|
||||||
protected $contact;
|
protected $contact;
|
||||||
|
|
||||||
public function getType()
|
public function getType()
|
||||||
{
|
{
|
||||||
return 'contact_lock';
|
return 'contact_lock';
|
||||||
@ -15,13 +16,13 @@ class Lock extends Event
|
|||||||
|
|
||||||
public function getDescription()
|
public function getDescription()
|
||||||
{
|
{
|
||||||
return 'Kunde gesperrt';
|
return 'Kunde deaktiviert';
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getData()
|
public function getData()
|
||||||
{
|
{
|
||||||
return array(
|
return array(
|
||||||
'contact' => $this->contact
|
'contact' => $this->contact,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -8,6 +8,7 @@ class UnLock extends Event
|
|||||||
{
|
{
|
||||||
/** @var string */
|
/** @var string */
|
||||||
protected $contact;
|
protected $contact;
|
||||||
|
|
||||||
public function getType()
|
public function getType()
|
||||||
{
|
{
|
||||||
return 'contact_unlock';
|
return 'contact_unlock';
|
||||||
@ -15,13 +16,13 @@ class UnLock extends Event
|
|||||||
|
|
||||||
public function getDescription()
|
public function getDescription()
|
||||||
{
|
{
|
||||||
return 'Kunde freigeschaltet';
|
return 'Kunde aktiviert';
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getData()
|
public function getData()
|
||||||
{
|
{
|
||||||
return array(
|
return array(
|
||||||
'contact' => $this->contact
|
'contact' => $this->contact,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -9,6 +9,7 @@ class Login extends Event
|
|||||||
{
|
{
|
||||||
/** @var string */
|
/** @var string */
|
||||||
protected $contact;
|
protected $contact;
|
||||||
|
|
||||||
public function getType()
|
public function getType()
|
||||||
{
|
{
|
||||||
return 'contact_login';
|
return 'contact_login';
|
||||||
@ -16,13 +17,13 @@ class Login extends Event
|
|||||||
|
|
||||||
public function getDescription()
|
public function getDescription()
|
||||||
{
|
{
|
||||||
return 'Kunde hat sich angemeldet';
|
return 'Kunde hat sich eingelogged';
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getData()
|
public function getData()
|
||||||
{
|
{
|
||||||
return array(
|
return array(
|
||||||
'contact' => $this->contact
|
'contact' => $this->contact,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -24,6 +24,7 @@ use PSC\Shop\EntityBundle\Entity\VoucherItem;
|
|||||||
use PSC\Shop\NewsBundle\Form\Backend\DeleteType;
|
use PSC\Shop\NewsBundle\Form\Backend\DeleteType;
|
||||||
use PSC\Shop\PaymentBundle\Form\Backend\PaymentType;
|
use PSC\Shop\PaymentBundle\Form\Backend\PaymentType;
|
||||||
use PSC\Shop\VoucherBundle\Form\Backend\VoucherType;
|
use PSC\Shop\VoucherBundle\Form\Backend\VoucherType;
|
||||||
|
use PSC\Shop\VoucherBundle\Form\Backend\CsvUploadType;
|
||||||
use PSC\System\PluginBundle\Form\Chain\Field;
|
use PSC\System\PluginBundle\Form\Chain\Field;
|
||||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
use Symfony\Component\HttpFoundation\File\UploadedFile;
|
use Symfony\Component\HttpFoundation\File\UploadedFile;
|
||||||
@ -254,4 +255,265 @@ class EditController extends AbstractController
|
|||||||
$response->headers->set('Content-Disposition', 'attachment; filename="export.csv"');
|
$response->headers->set('Content-Disposition', 'attachment; filename="export.csv"');
|
||||||
return $response;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -19,3 +19,4 @@ Numbertogenerate: Anzahl (zu generieren)
|
|||||||
Numbergenerated: Anzahl (generiert)
|
Numbergenerated: Anzahl (generiert)
|
||||||
Generate: Generieren
|
Generate: Generieren
|
||||||
Export: Exportieren
|
Export: Exportieren
|
||||||
|
Upload: Hochladen
|
||||||
@ -19,3 +19,4 @@ Numbertogenerate: Number (to generate)
|
|||||||
Numbergenerated: Number (to generated)
|
Numbergenerated: Number (to generated)
|
||||||
Generate: Generate
|
Generate: Generate
|
||||||
Export: Export
|
Export: Export
|
||||||
|
Upload: Upload
|
||||||
@ -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
|
||||||
@ -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
|
||||||
@ -13,7 +13,7 @@
|
|||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-wrap items-center gap-4 justify-end shrink-0 ml-auto">
|
<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">
|
<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" />
|
<path stroke-linecap="round" stroke-linejoin="round" d="M10.5 19.5L3 12m0 0l7.5-7.5M3 12h18" />
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
@ -13,7 +13,7 @@
|
|||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-wrap items-center gap-4 justify-end shrink-0 ml-auto">
|
<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">
|
<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" />
|
<path stroke-linecap="round" stroke-linejoin="round" d="M10.5 19.5L3 12m0 0l7.5-7.5M3 12h18" />
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
@ -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 %}
|
||||||
@ -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 %}
|
||||||
@ -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-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">
|
<td class="px-4 py-3 text-right">
|
||||||
<div class="flex flex-row gap-2 justify-end">
|
<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 #}
|
{# Export - BLAU #}
|
||||||
<a href="{{ path('psc_shop_voucher_backend_export', {uid: voucher.uid}) }}" target="_blank" title="{{'Export'|trans}}">
|
<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">
|
<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">
|
||||||
|
|||||||
@ -58,6 +58,8 @@
|
|||||||
</svg>
|
</svg>
|
||||||
<h5 class="text-lg font-semibold mb-2">PDF-Dateien hier ablegen</h5>
|
<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-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">
|
<input type="file" id="pdfInput" accept=".pdf" multiple class="hidden">
|
||||||
</div>
|
</div>
|
||||||
<div id="uploadedFilesList" class="mt-4"></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://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/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>
|
<script>
|
||||||
// Global variables
|
// Global variables
|
||||||
let uploadedFiles = [];
|
let uploadedFiles = [];
|
||||||
@ -942,12 +945,123 @@
|
|||||||
handleFiles(e.target.files);
|
handleFiles(e.target.files);
|
||||||
});
|
});
|
||||||
|
|
||||||
function handleFiles(files) {
|
/**
|
||||||
// Append new files instead of replacing
|
* Split a multi-page PDF into 2-page chunks
|
||||||
const newFiles = Array.from(files).filter(file => file.type === 'application/pdf');
|
* @param {File} file - The PDF file to split
|
||||||
uploadedFiles = [...uploadedFiles, ...newFiles];
|
* @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();
|
displayUploadedFiles();
|
||||||
document.getElementById('nextToStep2').disabled = uploadedFiles.length === 0;
|
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) {
|
function removeFile(index) {
|
||||||
@ -1053,30 +1167,18 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
// OCR functionality
|
// OCR functionality
|
||||||
async function processOCR(fileIndex) {
|
/**
|
||||||
const progressDiv = document.getElementById('ocr-progress');
|
* Perform OCR on a specific area of the canvas
|
||||||
|
* @param {HTMLCanvasElement} canvas - The source canvas
|
||||||
// Load the PDF if not already loaded
|
* @param {number} x - X coordinate
|
||||||
if (currentFileIndex !== fileIndex) {
|
* @param {number} y - Y coordinate
|
||||||
loadPDFInfo(fileIndex);
|
* @param {number} sourceWidth - Width of the area
|
||||||
}
|
* @param {number} sourceHeight - Height of the area
|
||||||
|
* @returns {Promise<Object>} { extractedNumber: string, previewDataUrl: string, tempCanvas: HTMLCanvasElement }
|
||||||
// Open modal and wait for render
|
*/
|
||||||
await openPreviewModal();
|
async function performOCRAtPosition(canvas, x, y, sourceWidth, sourceHeight) {
|
||||||
// Additional small delay to ensure rendering is complete
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 100));
|
|
||||||
|
|
||||||
progressDiv.classList.remove('hidden');
|
|
||||||
|
|
||||||
try {
|
|
||||||
const canvas = document.getElementById('modalCanvas');
|
|
||||||
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
|
// Create a temporary canvas for the OCR area
|
||||||
const tempCanvas = document.createElement('canvas');
|
const tempCanvas = document.createElement('canvas');
|
||||||
const sourceWidth = 150;
|
|
||||||
const sourceHeight = 50;
|
|
||||||
const scale = 4; // Scale up for better OCR
|
const scale = 4; // Scale up for better OCR
|
||||||
tempCanvas.width = sourceWidth * scale;
|
tempCanvas.width = sourceWidth * scale;
|
||||||
tempCanvas.height = sourceHeight * scale;
|
tempCanvas.height = sourceHeight * scale;
|
||||||
@ -1087,7 +1189,7 @@
|
|||||||
tempCtx.fillRect(0, 0, tempCanvas.width, tempCanvas.height);
|
tempCtx.fillRect(0, 0, tempCanvas.width, tempCanvas.height);
|
||||||
|
|
||||||
// Copy and scale up the specified area from the main 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);
|
tempCtx.drawImage(canvas, x, y, sourceWidth, sourceHeight, 0, 0, tempCanvas.width, tempCanvas.height);
|
||||||
|
|
||||||
// Image preprocessing for better OCR
|
// Image preprocessing for better OCR
|
||||||
const imageData = tempCtx.getImageData(0, 0, tempCanvas.width, tempCanvas.height);
|
const imageData = tempCtx.getImageData(0, 0, tempCanvas.width, tempCanvas.height);
|
||||||
@ -1108,23 +1210,15 @@
|
|||||||
}
|
}
|
||||||
tempCtx.putImageData(imageData, 0, 0);
|
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`;
|
|
||||||
|
|
||||||
// Save OCR preview for table (downscaled version)
|
// Save OCR preview for table (downscaled version)
|
||||||
const previewCanvas = document.createElement('canvas');
|
const previewCanvas = document.createElement('canvas');
|
||||||
previewCanvas.width = sourceWidth;
|
previewCanvas.width = sourceWidth;
|
||||||
previewCanvas.height = sourceHeight;
|
previewCanvas.height = sourceHeight;
|
||||||
const previewCtx = previewCanvas.getContext('2d');
|
const previewCtx = previewCanvas.getContext('2d');
|
||||||
previewCtx.drawImage(tempCanvas, 0, 0, sourceWidth, sourceHeight);
|
previewCtx.drawImage(tempCanvas, 0, 0, sourceWidth, sourceHeight);
|
||||||
ocrPreviews[fileIndex] = previewCanvas.toDataURL();
|
const previewDataUrl = previewCanvas.toDataURL();
|
||||||
|
|
||||||
console.log('Temp canvas data length:', tempCanvas.toDataURL().length);
|
console.log('Scanning at position:', { x, y, sourceWidth, sourceHeight });
|
||||||
|
|
||||||
const result = await Tesseract.recognize(
|
const result = await Tesseract.recognize(
|
||||||
tempCanvas,
|
tempCanvas,
|
||||||
@ -1146,15 +1240,342 @@
|
|||||||
const numberMatch = result.data.text.match(/\d+/);
|
const numberMatch = result.data.text.match(/\d+/);
|
||||||
const extractedNumber = numberMatch ? numberMatch[0] : '';
|
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');
|
||||||
|
|
||||||
|
// Load the PDF if not already loaded
|
||||||
|
if (currentFileIndex !== fileIndex) {
|
||||||
|
loadPDFInfo(fileIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open modal and wait for render
|
||||||
|
await openPreviewModal();
|
||||||
|
// Additional small delay to ensure rendering is complete
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 100));
|
||||||
|
|
||||||
|
progressDiv.classList.remove('hidden');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const canvas = document.getElementById('modalCanvas');
|
||||||
|
console.log('Canvas before OCR:', canvas.width, 'x', canvas.height);
|
||||||
|
console.log('Canvas has content:', canvas.toDataURL().length > 5000);
|
||||||
|
|
||||||
|
let finalResult = null;
|
||||||
|
let usedMethod = '';
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
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...');
|
||||||
|
|
||||||
|
const sourceWidth = 150;
|
||||||
|
const sourceHeight = 50;
|
||||||
|
const sourceY = 50;
|
||||||
|
|
||||||
|
// Try left position
|
||||||
|
const leftX = 20;
|
||||||
|
console.log('Trying LEFT position...');
|
||||||
|
const leftResult = await performOCRAtPosition(canvas, leftX, sourceY, sourceWidth, sourceHeight);
|
||||||
|
|
||||||
|
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');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display in debug canvas
|
||||||
|
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
|
||||||
|
ocrPreviews[fileIndex] = finalResult.previewDataUrl;
|
||||||
|
|
||||||
// Update the PDF data
|
// Update the PDF data
|
||||||
pdfData[fileIndex].ocrNumber = extractedNumber;
|
pdfData[fileIndex].ocrNumber = finalResult.extractedNumber;
|
||||||
pdfData[fileIndex].processed = true;
|
pdfData[fileIndex].processed = true;
|
||||||
|
|
||||||
// Update the input field
|
// Update the input field
|
||||||
document.getElementById(`ocr-input-${fileIndex}`).value = extractedNumber;
|
document.getElementById(`ocr-input-${fileIndex}`).value = finalResult.extractedNumber;
|
||||||
|
|
||||||
// Update the table
|
// Update the table
|
||||||
updatePDFTable();
|
updatePDFTable();
|
||||||
|
}
|
||||||
|
|
||||||
// Close modal
|
// Close modal
|
||||||
document.getElementById('previewModal').classList.add('hidden');
|
document.getElementById('previewModal').classList.add('hidden');
|
||||||
|
|||||||
@ -2219,6 +2219,11 @@ html {
|
|||||||
color: rgb(172 48 37 / var(--tw-text-opacity));
|
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{
|
.text-red-600{
|
||||||
--tw-text-opacity: 1;
|
--tw-text-opacity: 1;
|
||||||
color: rgb(220 38 38 / var(--tw-text-opacity));
|
color: rgb(220 38 38 / var(--tw-text-opacity));
|
||||||
@ -2827,6 +2832,11 @@ html {
|
|||||||
color: rgb(144 40 31 / var(--tw-text-opacity));
|
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{
|
.hover\:text-red-700:hover{
|
||||||
--tw-text-opacity: 1;
|
--tw-text-opacity: 1;
|
||||||
color: rgb(185 28 28 / var(--tw-text-opacity));
|
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));
|
--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{
|
.focus\:ring-psc-500:focus{
|
||||||
--tw-ring-opacity: 1;
|
--tw-ring-opacity: 1;
|
||||||
--tw-ring-color: rgb(234 100 27 / var(--tw-ring-opacity));
|
--tw-ring-color: rgb(234 100 27 / var(--tw-ring-opacity));
|
||||||
@ -2987,6 +3002,10 @@ html {
|
|||||||
--tw-ring-opacity: 0.5;
|
--tw-ring-opacity: 0.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.focus\:ring-offset-2:focus{
|
||||||
|
--tw-ring-offset-width: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
.disabled\:cursor-not-allowed:disabled{
|
.disabled\:cursor-not-allowed:disabled{
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
@ -3186,6 +3205,11 @@ html {
|
|||||||
color: rgb(34 197 94 / var(--tw-text-opacity));
|
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){
|
:is(.dark .dark\:text-red-200){
|
||||||
--tw-text-opacity: 1;
|
--tw-text-opacity: 1;
|
||||||
color: rgb(254 202 202 / var(--tw-text-opacity));
|
color: rgb(254 202 202 / var(--tw-text-opacity));
|
||||||
@ -3288,6 +3312,11 @@ html {
|
|||||||
color: rgb(220 102 91 / var(--tw-text-opacity));
|
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){
|
:is(.dark .dark\:hover\:text-red-400:hover){
|
||||||
--tw-text-opacity: 1;
|
--tw-text-opacity: 1;
|
||||||
color: rgb(248 113 113 / var(--tw-text-opacity));
|
color: rgb(248 113 113 / var(--tw-text-opacity));
|
||||||
@ -3433,10 +3462,6 @@ html {
|
|||||||
width: 41.666667%;
|
width: 41.666667%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.md\:w-9\/12{
|
|
||||||
width: 75%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.md\:w-fit{
|
.md\:w-fit{
|
||||||
width: -moz-fit-content;
|
width: -moz-fit-content;
|
||||||
width: fit-content;
|
width: fit-content;
|
||||||
@ -3514,10 +3539,6 @@ html {
|
|||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
}
|
}
|
||||||
|
|
||||||
.lg\:grid-cols-3{
|
|
||||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
||||||
}
|
|
||||||
|
|
||||||
.lg\:gap-8{
|
.lg\:gap-8{
|
||||||
gap: 2rem;
|
gap: 2rem;
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user