This commit is contained in:
Thomas Peterson 2026-01-13 18:22:22 +01:00
parent d598a9214f
commit a2187a48b8
9 changed files with 338 additions and 39 deletions

View File

@ -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: "Europe/Berlin" * default_timezone?: scalar|null, // Default: "UTC"
* 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: "Europe/Berlin" * default_timezone?: scalar|null, // Default: "UTC"
* cdata?: scalar|null, // Default: true * cdata?: scalar|null, // Default: true
* }, * },
* array_collection?: array{ * array_collection?: array{

View File

@ -25,11 +25,11 @@
{% block body %} {% block body %}
<div class="flex flex-col gap-6"> <div class="flex flex-col gap-6">
<div class="rounded-sm border bg-white px-7.5 py-6 shadow-lg dark:border-strokedark dark:bg-boxdark"> <div class="rounded-md border bg-white px-7.5 py-6 shadow-lg dark:border-strokedark dark:bg-boxdark">
<div class="mb-6 px-4"> <div class="mb-6 px-4">
{{ form_start(form, { 'attr': {'class': ''}}) }} {{ form_start(form, { 'attr': {'class': ''}}) }}
<div class="flex items-center gap-6"> <div class="flex items-center gap-6">
<label class="font-medium text-gray-700 text-sm min-w-fit"> <label class="font-medium text-gray-700 text-sm min-w-fit dark:text-gray-300">
Suche: Suche:
</label> </label>
<div class="flex-1 max-w-xl"> <div class="flex-1 max-w-xl">
@ -61,58 +61,67 @@
<div class="overflow-x-auto"> <div class="overflow-x-auto">
<table class="min-w-full text-sm"> <table class="min-w-full text-sm">
<thead class="bg-slate-100 border-t border-stroke"> <thead class="bg-slate-50 dark:bg-gray-800">
<tr> <tr class="border-b-2 border-gray-200 dark:border-gray-700">
<th class="px-2 py-3 text-left font-medium text-gray-700">{{ knp_pagination_sortable(pagination, 'Uid', 'contact.uid') }}</th> <th class="px-4 py-4 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider dark:text-gray-300">{{ knp_pagination_sortable(pagination, 'Uid', 'contact.uid') }}</th>
<th class="px-2 py-3 text-left font-medium text-gray-700">{{ knp_pagination_sortable(pagination, 'Loginname'|trans, 'contact.username') }}</th> <th class="px-4 py-4 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider dark:text-gray-300">{{ knp_pagination_sortable(pagination, 'Loginname'|trans, 'contact.username') }}</th>
<th class="px-2 py-3 text-left font-medium text-gray-700">{{ knp_pagination_sortable(pagination, 'generated'|trans, 'contact.createdAt') }}</th> <th class="px-4 py-4 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider dark:text-gray-300">{{ knp_pagination_sortable(pagination, 'generated'|trans, 'contact.createdAt') }}</th>
<th class="px-2 py-3 text-left font-medium text-gray-700">{{ knp_pagination_sortable(pagination, 'changed'|trans, 'contact.updatedAt') }}</th> <th class="px-4 py-4 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider dark:text-gray-300">{{ knp_pagination_sortable(pagination, 'changed'|trans, 'contact.updatedAt') }}</th>
<th class="px-2 py-3 text-left font-medium text-gray-700">{{ knp_pagination_sortable(pagination, 'firstname'|trans, 'contact.selffirstname') }}</th> <th class="px-4 py-4 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider dark:text-gray-300">{{ knp_pagination_sortable(pagination, 'firstname'|trans, 'contact.selffirstname') }}</th>
<th class="px-2 py-3 text-left font-medium text-gray-700">{{ knp_pagination_sortable(pagination, 'lastname'|trans, 'contact.lastname') }}</th> <th class="px-4 py-4 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider dark:text-gray-300">{{ knp_pagination_sortable(pagination, 'lastname'|trans, 'contact.lastname') }}</th>
<th class="px-2 py-3 text-left font-medium text-gray-700">{{ knp_pagination_sortable(pagination, 'street'|trans, 'contact.street') }}/{{ knp_pagination_sortable(pagination, 'housenumber'|trans, 'contact.houseNumber') }}</th> <th class="px-4 py-4 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider dark:text-gray-300">{{ knp_pagination_sortable(pagination, 'street'|trans, 'contact.street') }}/{{ knp_pagination_sortable(pagination, 'housenumber'|trans, 'contact.houseNumber') }}</th>
<th class="px-2 py-3 text-left font-medium text-gray-700">{{ knp_pagination_sortable(pagination, 'zip'|trans, 'contact.zip') }}</th> <th class="px-4 py-4 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider dark:text-gray-300">{{ knp_pagination_sortable(pagination, 'zip'|trans, 'contact.zip') }}</th>
<th class="px-2 py-3 text-left font-medium text-gray-700">{{ knp_pagination_sortable(pagination, 'city'|trans, 'contact.city') }}</th> <th class="px-4 py-4 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider dark:text-gray-300">{{ knp_pagination_sortable(pagination, 'city'|trans, 'contact.city') }}</th>
<th class="px-2 py-3 text-left font-medium text-gray-700">{{ 'virtual'|trans }}</th> <th class="px-4 py-4 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider dark:text-gray-300">{{ 'virtual'|trans }}</th>
<th class="px-2 py-3 text-left font-medium text-gray-700">{{ 'shops'|trans }}</th> <th class="px-4 py-4 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider dark:text-gray-300">{{ 'shops'|trans }}</th>
<th class="px-2 py-3 text-right font-medium text-gray-700">Aktionen</th> <th class="px-4 py-4 text-right text-xs font-semibold text-gray-600 uppercase tracking-wider dark:text-gray-300">Aktionen</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody class="bg-white dark:bg-boxdark">
{% for contact in pagination %} {% for contact in pagination %}
<tr class="border-t border-stroke hover:bg-gray-50"> <tr class="border-t border-gray-100 hover:bg-gray-50 dark:border-gray-700 dark:hover:bg-gray-800/50 transition-colors">
<td class="px-2 py-3 font-medium">{{ contact.id }}</td> <td class="px-4 py-3">
<td class="px-2 py-3"> <a href="{{ path("psc_shop_contact_backend_edit", {uuid: contact.uuid}) }}"
<a href="mailto:{{ contact.username }}" class="text-psc-500 hover:underline">{{ contact.username }}</a> class="inline-flex items-center px-2.5 py-1 rounded-md text-xs font-mono font-medium bg-gray-100 text-gray-800 border border-gray-200 hover:bg-psc-50 hover:text-psc-700 hover:border-psc-300 transition-colors dark:bg-gray-700 dark:text-gray-300 dark:border-gray-600 dark:hover:bg-psc-900 dark:hover:text-psc-300">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-3.5 h-3.5 mr-1.5 text-gray-500 dark:text-gray-400">
<path stroke-linecap="round" stroke-linejoin="round" d="M5.25 8.25h15m-16.5 7.5h15m-1.8-13.5l-3.9 19.5m-2.1-19.5l-3.9 19.5" />
</svg>
{{ contact.id }}
</a>
</td> </td>
<td class="px-2 py-3 whitespace-nowrap">{{ contact.createdAt|date('d.m.Y') }}</td> <td class="px-4 py-3">
<td class="px-2 py-3 whitespace-nowrap">{{ contact.updatedAt|date('d.m.Y') }}</td> <a href="mailto:{{ contact.username }}" class="text-psc-500 hover:underline dark:text-psc-400">{{ contact.username }}</a>
<td class="px-2 py-3">{{ contact.firstname }}</td> </td>
<td class="px-2 py-3">{{ contact.lastname }}</td> <td class="px-4 py-3 text-gray-900 dark:text-gray-100 whitespace-nowrap">{{ contact.createdAt|date('d.m.Y') }}</td>
<td class="px-2 py-3">{{ contact.street }} {{ contact.houseNumber }}</td> <td class="px-4 py-3 text-gray-900 dark:text-gray-100 whitespace-nowrap">{{ contact.updatedAt|date('d.m.Y') }}</td>
<td class="px-2 py-3">{{ contact.zip }}</td> <td class="px-4 py-3 text-gray-900 dark:text-gray-100">{{ contact.firstname }}</td>
<td class="px-2 py-3">{{ contact.city }}</td> <td class="px-4 py-3 font-medium text-gray-900 dark:text-gray-100">{{ contact.lastname }}</td>
<td class="px-2 py-3"> <td class="px-4 py-3 text-gray-900 dark:text-gray-100">{{ contact.street }} {{ contact.houseNumber }}</td>
<td class="px-4 py-3 text-gray-900 dark:text-gray-100">{{ contact.zip }}</td>
<td class="px-4 py-3 text-gray-900 dark:text-gray-100">{{ contact.city }}</td>
<td class="px-4 py-3">
{% if contact.virtual == 1 %} {% if contact.virtual == 1 %}
<div class="badge-yes">{{ 'yes'|trans }}</div> <span class="badge-yes">{{ 'yes'|trans }}</span>
{% else %} {% else %}
<div class="badge-no">{{ 'no'|trans }}</div> <span class="badge-no">{{ 'no'|trans }}</span>
{% endif %} {% endif %}
</td> </td>
<td class="px-2 py-3"> <td class="px-4 py-3">
{% if contact.shops|length > 1 %} {% if contact.shops|length > 1 %}
<div class="flex flex-wrap gap-1"> <div class="flex flex-wrap gap-1">
{% for shop in contact.shops %} {% for shop in contact.shops %}
<span class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-yellow-100 text-yellow-800">{{ shop.title }}</span> <span class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200">{{ shop.title }}</span>
{% endfor %} {% endfor %}
</div> </div>
{% else %} {% else %}
<div class="badge-no">{{ 'no'|trans }}</div> <span class="badge-no">{{ 'no'|trans }}</span>
{% endif %} {% endif %}
</td> </td>
<td class="px-2 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">
<a href="{{ path("psc_shop_contact_backend_edit", {uuid: contact.uuid}) }}" class=""> {# Bearbeiten - GRÜN #}
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="table-icon"> <a href="{{ path("psc_shop_contact_backend_edit", {uuid: contact.uuid}) }}" title="{{'edit'|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-green-600 hover:text-green-700 dark:text-green-500 dark:hover:text-green-400">
<path stroke-linecap="round" stroke-linejoin="round" d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L10.582 16.07a4.5 4.5 0 01-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 011.13-1.897l8.932-8.931zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0115.75 21H5.25A2.25 2.25 0 013 18.75V8.25A2.25 2.25 0 015.25 6H10" /> <path stroke-linecap="round" stroke-linejoin="round" d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L10.582 16.07a4.5 4.5 0 01-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 011.13-1.897l8.932-8.931zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0115.75 21H5.25A2.25 2.25 0 013 18.75V8.25A2.25 2.25 0 015.25 6H10" />
</svg> </svg>
</a> </a>

View File

@ -63,6 +63,32 @@ class GetSettings
$setting->setTabPositions($tabPositions); $setting->setTabPositions($tabPositions);
} }
// Ensure OCR markers are initialized
if (empty($setting->getOcrMarkers())) {
$ocrMarkers = [];
for ($i = 1; $i <= 4; $i++) {
$marker = new \Plugin\Custom\PSC\LaufkartenLayouter\Model\OcrMarker();
$marker->setType('dot');
$marker->setLabel("Dot {$i}");
$marker->setX(0.0);
$marker->setY(0.0);
$ocrMarkers[] = $marker;
}
$textMarker = new \Plugin\Custom\PSC\LaufkartenLayouter\Model\OcrMarker();
$textMarker->setType('text');
$textMarker->setLabel('Text Position');
$textMarker->setX(0.0);
$textMarker->setY(0.0);
$ocrMarkers[] = $textMarker;
$triangleMarker = new \Plugin\Custom\PSC\LaufkartenLayouter\Model\OcrMarker();
$triangleMarker->setType('triangle');
$triangleMarker->setLabel('Triangle (Pfeil nach oben)');
$triangleMarker->setX(0.0);
$triangleMarker->setY(0.0);
$ocrMarkers[] = $triangleMarker;
$setting->setOcrMarkers($ocrMarkers);
}
// Serialize settings to JSON // Serialize settings to JSON
$settingsJson = $serializer->serialize($setting, 'json'); $settingsJson = $serializer->serialize($setting, 'json');

View File

@ -56,6 +56,63 @@ class IndexController extends AbstractController
$tabPositions[] = $tabPosition; $tabPositions[] = $tabPosition;
} }
$setting->setTabPositions($tabPositions); $setting->setTabPositions($tabPositions);
// Initialize OCR markers (4 dots, 1 text, 1 triangle)
$ocrMarkers = [];
// 4 Dots
for ($i = 1; $i <= 4; $i++) {
$marker = new \Plugin\Custom\PSC\LaufkartenLayouter\Model\OcrMarker();
$marker->setType('dot');
$marker->setLabel("Dot {$i}");
$marker->setX(0.0);
$marker->setY(0.0);
$ocrMarkers[] = $marker;
}
// 1 Text
$textMarker = new \Plugin\Custom\PSC\LaufkartenLayouter\Model\OcrMarker();
$textMarker->setType('text');
$textMarker->setLabel('Text Position');
$textMarker->setX(0.0);
$textMarker->setY(0.0);
$ocrMarkers[] = $textMarker;
// 1 Triangle
$triangleMarker = new \Plugin\Custom\PSC\LaufkartenLayouter\Model\OcrMarker();
$triangleMarker->setType('triangle');
$triangleMarker->setLabel('Triangle (Pfeil nach oben)');
$triangleMarker->setX(0.0);
$triangleMarker->setY(0.0);
$ocrMarkers[] = $triangleMarker;
$setting->setOcrMarkers($ocrMarkers);
}
// Ensure OCR markers are initialized even when loading from DB
if (empty($setting->getOcrMarkers())) {
$ocrMarkers = [];
for ($i = 1; $i <= 4; $i++) {
$marker = new \Plugin\Custom\PSC\LaufkartenLayouter\Model\OcrMarker();
$marker->setType('dot');
$marker->setLabel("Dot {$i}");
$marker->setX(0.0);
$marker->setY(0.0);
$ocrMarkers[] = $marker;
}
$textMarker = new \Plugin\Custom\PSC\LaufkartenLayouter\Model\OcrMarker();
$textMarker->setType('text');
$textMarker->setLabel('Text Position');
$textMarker->setX(0.0);
$textMarker->setY(0.0);
$ocrMarkers[] = $textMarker;
$triangleMarker = new \Plugin\Custom\PSC\LaufkartenLayouter\Model\OcrMarker();
$triangleMarker->setType('triangle');
$triangleMarker->setLabel('Triangle (Pfeil nach oben)');
$triangleMarker->setX(0.0);
$triangleMarker->setY(0.0);
$ocrMarkers[] = $triangleMarker;
$setting->setOcrMarkers($ocrMarkers);
} }
// Create tab positions form // Create tab positions form

View File

@ -0,0 +1,50 @@
<?php
namespace Plugin\Custom\PSC\LaufkartenLayouter\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\NumberType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class OcrMarker extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('type', ChoiceType::class, [
'label' => 'Type',
'choices' => [
'Dot (Punkt)' => 'dot',
'Text' => 'text',
'Triangle (Dreieck)' => 'triangle',
],
'attr' => ['readonly' => true],
])
->add('label', TextType::class, [
'label' => 'Label',
'attr' => ['readonly' => true],
])
->add('x', NumberType::class, [
'label' => 'X Position',
'scale' => 1,
'html5' => true,
'attr' => ['step' => '0.1'],
])
->add('y', NumberType::class, [
'label' => 'Y Position',
'scale' => 1,
'html5' => true,
'attr' => ['step' => '0.1'],
]);
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => \Plugin\Custom\PSC\LaufkartenLayouter\Model\OcrMarker::class,
]);
}
}

View File

@ -101,6 +101,13 @@ class TabPositions extends AbstractType
'allow_delete' => true, 'allow_delete' => true,
'by_reference' => false, 'by_reference' => false,
'label' => 'Tab Positions', 'label' => 'Tab Positions',
])
->add('ocrMarkers', CollectionType::class, [
'entry_type' => OcrMarker::class,
'allow_add' => false,
'allow_delete' => false,
'by_reference' => false,
'label' => 'OCR Markers',
]); ]);
} }

View File

@ -0,0 +1,55 @@
<?php
namespace Plugin\Custom\PSC\LaufkartenLayouter\Model;
class OcrMarker
{
private string $type; // 'dot', 'text', 'triangle'
private string $label;
private float $x = 0.0;
private float $y = 0.0;
public function getType(): string
{
return $this->type;
}
public function setType(string $type): self
{
$this->type = $type;
return $this;
}
public function getLabel(): string
{
return $this->label;
}
public function setLabel(string $label): self
{
$this->label = $label;
return $this;
}
public function getX(): float
{
return $this->x;
}
public function setX(float $x): self
{
$this->x = $x;
return $this;
}
public function getY(): float
{
return $this->y;
}
public function setY(float $y): self
{
$this->y = $y;
return $this;
}
}

View File

@ -22,6 +22,11 @@ class Setting
*/ */
private array $tabPositions = []; private array $tabPositions = [];
/**
* @var OcrMarker[]
*/
private array $ocrMarkers = [];
public function getOrientation(): string public function getOrientation(): string
{ {
return $this->orientation; return $this->orientation;
@ -175,4 +180,15 @@ class Setting
$this->tabPositions = $tabPositions; $this->tabPositions = $tabPositions;
return $this; return $this;
} }
public function getOcrMarkers(): array
{
return $this->ocrMarkers;
}
public function setOcrMarkers(array $ocrMarkers): self
{
$this->ocrMarkers = $ocrMarkers;
return $this;
}
} }

View File

@ -38,6 +38,16 @@
aria-controls="positions" aria-controls="positions"
aria-selected="false">Tab Positionen</button> aria-selected="false">Tab Positionen</button>
</li> </li>
<li class="nav-item" role="presentation">
<button class="nav-link"
id="ocr-markers-tab"
data-bs-toggle="tab"
data-bs-target="#ocr-markers"
type="button"
role="tab"
aria-controls="ocr-markers"
aria-selected="false">OCR Marker</button>
</li>
</ul> </ul>
{{ form_start(tabPositionsForm) }} {{ form_start(tabPositionsForm) }}
@ -222,6 +232,75 @@
</div> </div>
</div> </div>
</div> </div>
<div class="tab-pane"
id="ocr-markers"
role="tabpanel"
aria-labelledby="ocr-markers-tab">
<div class="row">
<div class="col-12">
<div class="card d-flex">
<div class="card-header">
OCR Marker Positionen
<span class="badge bg-info float-end">4 Dots + 1 Text + 1 Dreieck</span>
</div>
<div class="card-body">
<p class="text-muted mb-3">
<i class="fas fa-info-circle"></i>
Definieren Sie die Positionen für OCR-Erkennung: 4 Punkte (Dots), 1 Text-Position und 1 Dreieck (Pfeil nach oben).
Diese Marker werden für die automatische Erkennung der Meldegruppe verwendet.
</p>
<div class="table-responsive">
<table class="table table-striped table-bordered">
<thead class="table-dark">
<tr>
<th style="width: 15%">Type</th>
<th style="width: 25%">Label</th>
<th style="width: 30%">X Position ({{ setting.unit }})</th>
<th style="width: 30%">Y Position ({{ setting.unit }})</th>
</tr>
</thead>
<tbody>
{% for ocrMarker in tabPositionsForm.ocrMarkers %}
<tr>
<td class="align-middle">
{% if ocrMarker.vars.data.type == 'dot' %}
<span class="badge bg-primary"><i class="fas fa-circle"></i> Dot</span>
{% elseif ocrMarker.vars.data.type == 'text' %}
<span class="badge bg-success"><i class="fas fa-font"></i> Text</span>
{% elseif ocrMarker.vars.data.type == 'triangle' %}
<span class="badge bg-warning"><i class="fas fa-caret-up"></i> Dreieck</span>
{% endif %}
{{ form_widget(ocrMarker.type, {'attr': {'class': 'd-none'}}) }}
</td>
<td class="align-middle">
<strong>{{ ocrMarker.vars.data.label }}</strong>
{{ form_widget(ocrMarker.label, {'attr': {'class': 'd-none'}}) }}
</td>
<td>
{{ form_widget(ocrMarker.x, {'attr': {'class': 'form-control', 'placeholder': '0.0'}}) }}
</td>
<td>
{{ form_widget(ocrMarker.y, {'attr': {'class': 'form-control', 'placeholder': '0.0'}}) }}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<div class="row mt-3">
<div class="col-12 text-end">
<button type="submit" class="btn btn-success">
<i class="fas fa-save"></i> OCR Marker speichern
</button>
</div>
</div>
</div>
</div> </div>
{{ form_end(tabPositionsForm) }} {{ form_end(tabPositionsForm) }}