This commit is contained in:
Thomas Peterson 2026-01-06 10:09:37 +01:00
parent 47057ea7a3
commit 7e46c1fc5d
24 changed files with 1752 additions and 636 deletions

View File

@ -64,10 +64,10 @@ server {
try_files $uri @sfFront;
}
location /w2p/ {
proxy_pass http://tp:8080/w2p/;
proxy_temp_path /tmp/proxy;
}
# location /w2p/ {
# proxy_pass http://tp:8080/w2p/;
# proxy_temp_path /tmp/proxy;
# }
location @sfFront { # Symfony
if ($request_method = 'OPTIONS') {

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

View File

@ -1,201 +1,166 @@
{% extends 'backend_base.html.twig' %}
{% extends 'backend_tailwind_base.html.twig' %}
{% form_theme form 'tailwind_formtheme.html.twig' %}
{% trans_default_domain 'core_voucher_edit' %}
{% 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="M16.5 6v.75m0 3v.75m0 3v.75m0 3V18m-9-5.25h5.25M7.5 15h3M3.375 5.25c-.621 0-1.125.504-1.125 1.125v3.026a2.999 2.999 0 010 5.198v3.026c0 .621.504 1.125 1.125 1.125h17.25c.621 0 1.125-.504 1.125-1.125v-3.026a2.999 2.999 0 010-5.198V6.375c0-.621-.504-1.125-1.125-1.125H3.375z" />
</svg>
{{'Voucher'|trans}} <span class="text-gray-500">{{'create'|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-psc-500 hover:bg-psc-600 hover:ring-2 hover:ring-psc-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: ''}}) }}
<div class="header">
<div class="row">
<div class="col-xs-12 col-sm-6 col-md-6 col-lg-6">
<h3>
<i class="fa-fw fa fa-id-card"></i>
{{'Voucher'|trans}} <span>>
{{'create'|trans}} </span>
</h3>
</div>
<div class="col-xs-12 col-sm-6 col-md-6 col-lg-6 text-end">
<a href="{{ path("psc_shop_voucher_backend_list") }}" class="btn btn-default btn-sm"><i class="fa fa-lg fa-fw fa-arrow-left"></i> {{'back'|trans}}</a>
</div>
</div>
</div>
<div class="tab-group flex-none md:flex w-full" data-dui-orientation="vertical">
{# Vertical Tab Navigation #}
<div role="tablist" class="relative mr-5 rounded-md flex flex-col p-1 w-full md:w-2/12">
<div class="tab-indicator absolute left-0 w-1 bg-psc-500 transition-transform duration-300"></div>
<a href="#" class="tab-link flex items-center text-sm active px-4 py-2 relative" data-dui-tab-target="general">
<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="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.324.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 0 1 1.37.49l1.296 2.247a1.125 1.125 0 0 1-.26 1.431l-1.003.827c-.293.241-.438.613-.43.992a7.723 7.723 0 0 1 0 .255c-.008.378.137.75.43.991l1.004.827c.424.35.534.955.26 1.43l-1.298 2.247a1.125 1.125 0 0 1-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.47 6.47 0 0 1-.22.128c-.331.183-.581.495-.644.869l-.213 1.281c-.09.543-.56.94-1.11.94h-2.594c-.55 0-1.019-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 0 1-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 0 1-1.369-.49l-1.297-2.247a1.125 1.125 0 0 1 .26-1.431l1.004-.827c.292-.24.437-.613.43-.991a6.932 6.932 0 0 1 0-.255c.007-.38-.138-.751-.43-.992l-1.004-.827a1.125 1.125 0 0 1-.26-1.43l1.297-2.247a1.125 1.125 0 0 1 1.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.086.22-.128.332-.183.582-.495.644-.869l.214-1.28Z" />
<path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" />
</svg>
{{'General'|trans}}
</a>
<a href="#" class="tab-link flex items-center text-sm px-4 py-2 relative" data-dui-tab-target="validity">
<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="M6.75 3v2.25M17.25 3v2.25M3 18.75V7.5a2.25 2.25 0 0 1 2.25-2.25h13.5A2.25 2.25 0 0 1 21 7.5v11.25m-18 0A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75m-18 0v-7.5A2.25 2.25 0 0 1 5.25 9h13.5A2.25 2.25 0 0 1 21 11.25v7.5" />
</svg>
Gültigkeit
</a>
<a href="#" class="tab-link flex items-center text-sm px-4 py-2 relative" data-dui-tab-target="filter">
<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="M12 3c2.755 0 5.455.232 8.083.678.533.09.917.556.917 1.096v1.044a2.25 2.25 0 0 1-.659 1.591l-5.432 5.432a2.25 2.25 0 0 0-.659 1.591v2.927a2.25 2.25 0 0 1-1.244 2.013L9.75 21v-6.568a2.25 2.25 0 0 0-.659-1.591L3.659 7.409A2.25 2.25 0 0 1 3 5.818V4.774c0-.54.384-1.006.917-1.096A48.32 48.32 0 0 1 12 3Z" />
</svg>
{{'Filter'|trans}}
</a>
<div class="body">
{{ form_start(form, { 'attr': {'class': ''}}) }}
<div class="panel">
<div class="header">
<h4>{{ voucher.title }}</h4>
</div>
<div class="body">
<div class="row">
<div class="col-md-2">
<ul class="nav nav-pills flex-column" role="tablist">
<li class="nav-item">
<a class="nav-link active" data-bs-toggle="tab" href="#all" role="tab">{{'General'|trans}}</a>
</li>
<li class="nav-item">
<a class="nav-link" data-bs-toggle="tab" href="#add" role="tab">{{'Others'|trans}}</a>
</li>
{% for customGroup in customGroups %}
<li class="nav-item">
<a class="nav-link" data-bs-toggle="tab" href="#{{ customGroup.id }}" role="tab">{{ customGroup.title }}</a>
</li>
<a href="#" class="tab-link flex items-center text-sm px-4 py-2 relative" data-dui-tab-target="{{ customGroup.id }}">
<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="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.324.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 0 1 1.37.49l1.296 2.247a1.125 1.125 0 0 1-.26 1.431l-1.003.827c-.293.241-.438.613-.43.992a7.723 7.723 0 0 1 0 .255c-.008.378.137.75.43.991l1.004.827c.424.35.534.955.26 1.43l-1.298 2.247a1.125 1.125 0 0 1-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.47 6.47 0 0 1-.22.128c-.331.183-.581.495-.644.869l-.213 1.281c-.09.543-.56.94-1.11.94h-2.594c-.55 0-1.019-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 0 1-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 0 1-1.369-.49l-1.297-2.247a1.125 1.125 0 0 1 .26-1.431l1.004-.827c.292-.24.437-.613.43-.991a6.932 6.932 0 0 1 0-.255c.007-.38-.138-.751-.43-.992l-1.004-.827a1.125 1.125 0 0 1-.26-1.43l1.297-2.247a1.125 1.125 0 0 1 1.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.086.22-.128.332-.183.582-.495.644-.869l.214-1.28Z" />
<path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" />
</svg>
{{ customGroup.title }}
</a>
{% endfor %}
</ul>
</div>
<div class="col-md-10">
<div class="tab-content">
<div class="tab-pane active" id="all" role="tabpanel">
<div class="row">
<div class="col-md-4">
<div class="row mb-3">
<label class="col-md-3 form-control-label">{{ form_label(form.title) }}</label>
<div class="col-md-9">
{{ form_widget(form.title) }}
{# Tab Content Area #}
<div class="rounded-md w-full border bg-white p-5 shadow-lg dark:border-strokedark dark:bg-boxdark">
{# General Tab #}
<div id="general" class="tab-content w-full text-stone-500 text-sm block">
<h6 class="text-sm mt-3 mb-6 font-bold uppercase">{{'General'|trans}}</h6>
<div class="flex flex-wrap">
<div class="w-full lg:w-4/12 px-4">
{{ form_row(form.title) }}
</div>
</div>
</div>
<div class="col-md-4">
<div class="row">
<div class="col-md-6">{{ form_widget(form.enable) }}</div>
<div class="w-full lg:w-4/12 px-4">
{{ form_row(form.enable) }}
</div>
</div>
<div class="flex flex-wrap">
<div class="w-full lg:w-4/12 px-4">
{{ form_row(form.percent) }}
</div>
<div class="row">
<div class="col-md-4">
<div class="row mb-3">
<label class="col-md-3 form-control-label">{{ form_label(form.fromDate) }}</label>
<div class="col-md-9">
{{ form_widget(form.fromDate) }}
<div class="w-full lg:w-4/12 px-4">
{{ form_row(form.value) }}
</div>
<div class="w-full lg:w-4/12 px-4">
{{ form_row(form.mode) }}
</div>
</div>
<div class="flex flex-wrap">
<div class="w-full lg:w-4/12 px-4">
{{ form_row(form.more) }}
</div>
<div class="w-full lg:w-4/12 px-4">
{{ form_row(form.count) }}
</div>
<div class="w-full lg:w-4/12 px-4">
{{ form_row(form.code) }}
</div>
</div>
<div class="flex flex-wrap">
<div class="w-full lg:w-6/12 px-4">
{{ form_row(form.zeroPayment) }}
</div>
<div class="w-full lg:w-6/12 px-4">
{{ form_row(form.zeroShipping) }}
</div>
</div>
</div>
<div class="col-md-4">
<div class="row mb-3">
<label class="col-md-3 form-control-label">{{ form_label(form.toDate) }}</label>
<div class="col-md-9">
{{ form_widget(form.toDate) }}
{# Validity Tab #}
<div id="validity" class="tab-content w-full text-stone-500 text-sm hidden">
<h6 class="text-sm mt-3 mb-6 font-bold uppercase">Gültigkeit</h6>
<div class="flex flex-wrap">
<div class="w-full lg:w-6/12 px-4">
{{ form_row(form.fromDate) }}
</div>
<div class="w-full lg:w-6/12 px-4">
{{ form_row(form.toDate) }}
</div>
</div>
<div class="flex flex-wrap">
<div class="w-full lg:w-4/12 px-4">
{{ form_row(form.minBasketValue) }}
</div>
</div>
</div>
{# Filter Tab #}
<div id="filter" class="tab-content w-full text-stone-500 text-sm hidden">
<h6 class="text-sm mt-3 mb-6 font-bold uppercase">{{'Filter'|trans}}</h6>
<div class="flex flex-wrap">
<div class="w-full lg:w-6/12 px-4">
{{ form_row(form.payment) }}
</div>
<div class="row">
<div class="col-md-4">
<div class="row mb-3">
<label class="col-md-3 form-control-label">{{ form_label(form.percent) }}</label>
<div class="col-md-9">
{{ form_widget(form.percent) }}
</div>
</div>
</div>
<div class="col-md-4">
<div class="row mb-3">
<label class="col-md-3 form-control-label">{{ form_label(form.value) }}</label>
<div class="col-md-9">
{{ form_widget(form.value) }}
</div>
</div>
</div>
<div class="col-md-4">
<div class="row mb-3">
<label class="col-md-3 form-control-label">{{ form_label(form.mode) }}</label>
<div class="col-md-9">
{{ form_widget(form.mode) }}
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-md-4">
<div class="row mb-3">
<label class="col-md-3 form-control-label">{{ form_label(form.more) }}</label>
<div class="col-md-9">
{{ form_widget(form.more) }}
</div>
</div>
</div>
<div class="col-md-4">
<div class="row mb-3">
<label class="col-md-3 form-control-label">{{ form_label(form.count) }}</label>
<div class="col-md-9">
{{ form_widget(form.count) }}
</div>
</div>
</div>
<div class="col-md-4">
<div class="row mb-3">
<label class="col-md-3 form-control-label">{{ form_label(form.code) }}</label>
<div class="col-md-9">
{{ form_widget(form.code) }}
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-md-4">
<div class="row mb-3">
<label class="col-md-3 form-control-label">{{ form_label(form.payment) }}</label>
<div class="col-md-9">
{{ form_widget(form.payment) }}
</div>
</div>
</div>
<div class="col-md-4">
<div class="row mb-3">
<label class="col-md-3 form-control-label">{{ form_label(form.shipping) }}</label>
<div class="col-md-9">
{{ form_widget(form.shipping) }}
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-md-4">
<div class="row mb-3">
<label class="col-md-3 form-control-label">{{ form_label(form.zeroPayment) }}</label>
<div class="col-md-9">
{{ form_widget(form.zeroPayment) }}
</div>
</div>
</div>
<div class="col-md-4">
<div class="row mb-3">
<label class="col-md-3 form-control-label">{{ form_label(form.zeroShipping) }}</label>
<div class="col-md-9">
{{ form_widget(form.zeroShipping) }}
</div>
</div>
</div>
<div class="col-md-4">
<div class="row mb-3">
<label class="col-md-3 form-control-label">{{ form_label(form.minBasketValue) }}</label>
<div class="col-md-9">
{{ form_widget(form.minBasketValue) }}
</div>
</div>
</div>
</div>
</div>
<div class="tab-pane" id="add" role="tabpanel">
<div class="row">
<div class="col-md-6">
<div class="row mb-3">
<label class="col-md-3 form-control-label">{{ form_label(form.productIds) }}</label>
<div class="col-md-9">
{{ form_widget(form.productIds) }}
</div>
</div>
</div>
<div class="col-md-6">
<div class="row mb-3">
<label class="col-md-3 form-control-label">{{ form_label(form.productGroupIds) }}</label>
<div class="col-md-9">
{{ form_widget(form.productGroupIds) }}
</div>
</div>
<div class="w-full lg:w-6/12 px-4">
{{ form_row(form.shipping) }}
</div>
</div>
<div class="flex flex-wrap">
<div class="w-full lg:w-6/12 px-4">
{{ form_row(form.productIds) }}
</div>
<div class="w-full lg:w-6/12 px-4">
{{ form_row(form.productGroupIds) }}
</div>
</div>
</div>
{# Custom Tabs #}
{% for customGroup in customGroups %}
<div class="tab-pane" id="{{ customGroup.id }}" role="tabpanel">
<div id="{{ customGroup.id }}" class="tab-content w-full text-stone-500 text-sm hidden">
<h6 class="text-sm mt-3 mb-6 font-bold uppercase">{{ customGroup.title }}</h6>
{% for customField in customFields %}
{% if customField.group == customGroup.id and customField.getTemplate %}
{{ include(customField.getTemplate, { 'form': form }) }}
@ -205,22 +170,19 @@
{% endfor %}
</div>
</div>
</div>
</div>
{# Save Button outside Card #}
<div class="text-end my-2">
{{ form_widget(form.save, {
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>
<div class="panel">
<div class="body">
<div class="row mb-3">
<div class="col-md-offset-1 col-md-11">
{{ form_widget(form.save, {attr: {class: 'btn btn-primary btn-sm'}}) }}
</div>
</div>
</div>
</div>
{{ form_end(form) }}
</div>
</div>
{{ summernote_mediabundle_init('default') }}
{{ summernote_mediabundle_init('default') }}
{% endblock %}

View File

@ -1,39 +1,82 @@
{% extends 'backend_base.html.twig' %}
{% extends 'backend_tailwind_base.html.twig' %}
{% trans_default_domain 'core_voucher_delete' %}
{% block body %}
<div class="header">
<div class="row">
<div class="col-xs-12 col-sm-6 col-md-6 col-lg-6">
<h3>
<i class="fa-fw fa fa-id-card"></i>
{{'Voucher'|trans}} <span>>
{{'del'|trans}}</span>
</h3>
</div>
</div>
</div>
<div class="body">
<div class="panel">
<div class="header">
<h4>{{'DelVoucher'|trans}}?</h4>
</div>
<div class="body">
<h5>{{ voucher.title }}</h5>
{{ form_start(form, { 'attr': {'class': ''}}) }}
<div class="row mb-3">
<label class="col-md-1 form-control-label"></label>
<div class="col-md-1">
{{ form_widget(form.yes, {attr: {class: 'btn btn-lg btn-warning btn-sm'}}) }}
</div>
<div class="col-md-1">
{{ form_widget(form.no, {attr: {class: 'btn btn-lg btn-primary btn-sm'}}) }}
</div>
</div>
{{ form_end(form) }}
{% 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="M16.5 6v.75m0 3v.75m0 3v.75m0 3V18m-9-5.25h5.25M7.5 15h3M3.375 5.25c-.621 0-1.125.504-1.125 1.125v3.026a2.999 2.999 0 010 5.198v3.026c0 .621.504 1.125 1.125 1.125h17.25c.621 0 1.125-.504 1.125-1.125v-3.026a2.999 2.999 0 010-5.198V6.375c0-.621-.504-1.125-1.125-1.125H3.375z" />
</svg>
{{'Voucher'|trans}} <span class="text-gray-500">{{'del'|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-psc-500 hover:bg-psc-600 hover:ring-2 hover:ring-psc-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="flex flex-col gap-6">
{{ form_start(form, { 'attr': {'class': ''}}) }}
<div class="rounded-md border border-red-200 bg-white shadow-lg dark:border-strokedark dark:bg-boxdark">
{# Card Header #}
<div class="border-b border-red-200 bg-red-50 px-7.5 py-4 dark:border-strokedark dark:bg-red-900/20">
<div class="flex items-center gap-3">
<svg class="w-6 h-6 text-red-600 dark:text-red-500" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z" />
</svg>
<h3 class="text-xl font-medium text-red-900 dark:text-red-200">
{{'DelVoucher'|trans}}?
</h3>
</div>
</div>
{# Card Body #}
<div class="px-7.5 py-6">
<div class="p-4 bg-yellow-50 rounded-md border border-yellow-200 mb-4">
<div class="flex gap-3">
<svg class="w-5 h-5 text-yellow-600 flex-shrink-0 mt-0.5" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z" />
</svg>
<p class="text-sm text-yellow-800">
{{'This action cannot be undone'|trans}}.
</p>
</div>
</div>
<div class="p-4 bg-gray-50 rounded-md border border-gray-200 dark:bg-boxdark-2 dark:border-strokedark">
<h5 class="text-lg font-medium text-gray-900 dark:text-bodydark mb-2">
{{ voucher.title }}
</h5>
<p class="text-sm text-gray-600 dark:text-gray-400">
UID: {{ voucher.uid }}
</p>
</div>
</div>
{# Card Footer #}
<div class="border-t border-gray-200 px-7.5 py-4 bg-gray-50 dark:border-strokedark dark:bg-boxdark-2">
<div class="flex justify-end gap-3">
{{ form_widget(form.no, {
attr: {
class: 'inline-flex items-center justify-center py-2 gap-2 font-medium rounded-md px-4 text-sm text-psc-600 border border-psc-500 bg-white hover:bg-gray-50 hover:ring-2 hover:ring-psc-500 hover:ring-offset-1 shadow-sm'
}
}) }}
{{ form_widget(form.yes, {
attr: {
class: 'inline-flex items-center justify-center py-2 gap-2 font-medium rounded-md px-4 text-sm text-white shadow-lg bg-red-600 hover:bg-red-700 hover:ring-2 hover:ring-red-500 hover:ring-offset-2'
}
}) }}
</div>
</div>
</div>
{{ form_end(form) }}
</div>
{% endblock %}

View File

@ -1,201 +1,166 @@
{% extends 'backend_base.html.twig' %}
{% extends 'backend_tailwind_base.html.twig' %}
{% form_theme form 'tailwind_formtheme.html.twig' %}
{% trans_default_domain 'core_voucher_edit' %}
{% 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="M16.5 6v.75m0 3v.75m0 3v.75m0 3V18m-9-5.25h5.25M7.5 15h3M3.375 5.25c-.621 0-1.125.504-1.125 1.125v3.026a2.999 2.999 0 010 5.198v3.026c0 .621.504 1.125 1.125 1.125h17.25c.621 0 1.125-.504 1.125-1.125v-3.026a2.999 2.999 0 010-5.198V6.375c0-.621-.504-1.125-1.125-1.125H3.375z" />
</svg>
{{'Voucher'|trans}} <span class="text-gray-500">{{'edit'|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-psc-500 hover:bg-psc-600 hover:ring-2 hover:ring-psc-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: ''}}) }}
<div class="header">
<div class="row">
<div class="col-xs-12 col-sm-6 col-md-6 col-lg-6">
<h3>
<i class="fa-fw fa fa-id-card"></i>
{{'Voucher'|trans}} <span>>
{{'edit'|trans}} </span>
</h3>
</div>
<div class="col-xs-12 col-sm-6 col-md-6 col-lg-6 text-end">
<a href="{{ path("psc_shop_voucher_backend_list") }}" class="btn btn-default btn-sm"><i class="fa fa-lg fa-fw fa-arrow-left"></i> {{'back'|trans}}</a>
</div>
</div>
</div>
<div class="tab-group flex-none md:flex w-full" data-dui-orientation="vertical">
{# Vertical Tab Navigation #}
<div role="tablist" class="relative mr-5 rounded-md flex flex-col p-1 w-full md:w-2/12">
<div class="tab-indicator absolute left-0 w-1 bg-psc-500 transition-transform duration-300"></div>
<a href="#" class="tab-link flex items-center text-sm active px-4 py-2 relative" data-dui-tab-target="general">
<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="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.324.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 0 1 1.37.49l1.296 2.247a1.125 1.125 0 0 1-.26 1.431l-1.003.827c-.293.241-.438.613-.43.992a7.723 7.723 0 0 1 0 .255c-.008.378.137.75.43.991l1.004.827c.424.35.534.955.26 1.43l-1.298 2.247a1.125 1.125 0 0 1-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.47 6.47 0 0 1-.22.128c-.331.183-.581.495-.644.869l-.213 1.281c-.09.543-.56.94-1.11.94h-2.594c-.55 0-1.019-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 0 1-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 0 1-1.369-.49l-1.297-2.247a1.125 1.125 0 0 1 .26-1.431l1.004-.827c.292-.24.437-.613.43-.991a6.932 6.932 0 0 1 0-.255c.007-.38-.138-.751-.43-.992l-1.004-.827a1.125 1.125 0 0 1-.26-1.43l1.297-2.247a1.125 1.125 0 0 1 1.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.086.22-.128.332-.183.582-.495.644-.869l.214-1.28Z" />
<path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" />
</svg>
{{'General'|trans}}
</a>
<a href="#" class="tab-link flex items-center text-sm px-4 py-2 relative" data-dui-tab-target="validity">
<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="M6.75 3v2.25M17.25 3v2.25M3 18.75V7.5a2.25 2.25 0 0 1 2.25-2.25h13.5A2.25 2.25 0 0 1 21 7.5v11.25m-18 0A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75m-18 0v-7.5A2.25 2.25 0 0 1 5.25 9h13.5A2.25 2.25 0 0 1 21 11.25v7.5" />
</svg>
Gültigkeit
</a>
<a href="#" class="tab-link flex items-center text-sm px-4 py-2 relative" data-dui-tab-target="filter">
<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="M12 3c2.755 0 5.455.232 8.083.678.533.09.917.556.917 1.096v1.044a2.25 2.25 0 0 1-.659 1.591l-5.432 5.432a2.25 2.25 0 0 0-.659 1.591v2.927a2.25 2.25 0 0 1-1.244 2.013L9.75 21v-6.568a2.25 2.25 0 0 0-.659-1.591L3.659 7.409A2.25 2.25 0 0 1 3 5.818V4.774c0-.54.384-1.006.917-1.096A48.32 48.32 0 0 1 12 3Z" />
</svg>
{{'Filter'|trans}}
</a>
<div class="body">
{{ form_start(form, { 'attr': {'class': ''}}) }}
<div class="panel">
<div class="header">
<h4>{{ voucher.title }}</h4>
</div>
<div class="body">
<div class="row">
<div class="col-md-2">
<ul class="nav nav-pills flex-column" role="tablist">
<li class="nav-item">
<a class="nav-link active" data-bs-toggle="tab" href="#all" role="tab">{{'General'|trans}}</a>
</li>
<li class="nav-item">
<a class="nav-link" data-bs-toggle="tab" href="#add" role="tab">{{'Others'|trans}}</a>
</li>
{% for customGroup in customGroups %}
<li class="nav-item">
<a class="nav-link" data-bs-toggle="tab" href="#{{ customGroup.id }}" role="tab">{{ customGroup.title }}</a>
</li>
<a href="#" class="tab-link flex items-center text-sm px-4 py-2 relative" data-dui-tab-target="{{ customGroup.id }}">
<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="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.324.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 0 1 1.37.49l1.296 2.247a1.125 1.125 0 0 1-.26 1.431l-1.003.827c-.293.241-.438.613-.43.992a7.723 7.723 0 0 1 0 .255c-.008.378.137.75.43.991l1.004.827c.424.35.534.955.26 1.43l-1.298 2.247a1.125 1.125 0 0 1-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.47 6.47 0 0 1-.22.128c-.331.183-.581.495-.644.869l-.213 1.281c-.09.543-.56.94-1.11.94h-2.594c-.55 0-1.019-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 0 1-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 0 1-1.369-.49l-1.297-2.247a1.125 1.125 0 0 1 .26-1.431l1.004-.827c.292-.24.437-.613.43-.991a6.932 6.932 0 0 1 0-.255c.007-.38-.138-.751-.43-.992l-1.004-.827a1.125 1.125 0 0 1-.26-1.43l1.297-2.247a1.125 1.125 0 0 1 1.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.086.22-.128.332-.183.582-.495.644-.869l.214-1.28Z" />
<path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" />
</svg>
{{ customGroup.title }}
</a>
{% endfor %}
</ul>
</div>
<div class="col-md-10">
<div class="tab-content">
<div class="tab-pane active" id="all" role="tabpanel">
<div class="row">
<div class="col-md-4">
<div class="row mb-3">
<label class="col-md-3 form-control-label">{{ form_label(form.title) }}</label>
<div class="col-md-9">
{{ form_widget(form.title) }}
{# Tab Content Area #}
<div class="rounded-md w-full border bg-white p-5 shadow-lg dark:border-strokedark dark:bg-boxdark">
{# General Tab #}
<div id="general" class="tab-content w-full text-stone-500 text-sm block">
<h6 class="text-sm mt-3 mb-6 font-bold uppercase">{{'General'|trans}}</h6>
<div class="flex flex-wrap">
<div class="w-full lg:w-4/12 px-4">
{{ form_row(form.title) }}
</div>
</div>
</div>
<div class="col-md-4">
<div class="row">
<div class="col-md-6">{{ form_widget(form.enable) }}</div>
<div class="w-full lg:w-4/12 px-4">
{{ form_row(form.enable) }}
</div>
</div>
<div class="flex flex-wrap">
<div class="w-full lg:w-4/12 px-4">
{{ form_row(form.percent) }}
</div>
<div class="row">
<div class="col-md-4">
<div class="row mb-3">
<label class="col-md-3 form-control-label">{{ form_label(form.fromDate) }}</label>
<div class="col-md-9">
{{ form_widget(form.fromDate) }}
<div class="w-full lg:w-4/12 px-4">
{{ form_row(form.value) }}
</div>
<div class="w-full lg:w-4/12 px-4">
{{ form_row(form.mode) }}
</div>
</div>
<div class="flex flex-wrap">
<div class="w-full lg:w-4/12 px-4">
{{ form_row(form.more) }}
</div>
<div class="w-full lg:w-4/12 px-4">
{{ form_row(form.count) }}
</div>
<div class="w-full lg:w-4/12 px-4">
{{ form_row(form.code) }}
</div>
</div>
<div class="flex flex-wrap">
<div class="w-full lg:w-6/12 px-4">
{{ form_row(form.zeroPayment) }}
</div>
<div class="w-full lg:w-6/12 px-4">
{{ form_row(form.zeroShipping) }}
</div>
</div>
</div>
<div class="col-md-4">
<div class="row mb-3">
<label class="col-md-3 form-control-label">{{ form_label(form.toDate) }}</label>
<div class="col-md-9">
{{ form_widget(form.toDate) }}
{# Validity Tab #}
<div id="validity" class="tab-content w-full text-stone-500 text-sm hidden">
<h6 class="text-sm mt-3 mb-6 font-bold uppercase">Gültigkeit</h6>
<div class="flex flex-wrap">
<div class="w-full lg:w-6/12 px-4">
{{ form_row(form.fromDate) }}
</div>
<div class="w-full lg:w-6/12 px-4">
{{ form_row(form.toDate) }}
</div>
</div>
<div class="flex flex-wrap">
<div class="w-full lg:w-4/12 px-4">
{{ form_row(form.minBasketValue) }}
</div>
</div>
</div>
{# Filter Tab #}
<div id="filter" class="tab-content w-full text-stone-500 text-sm hidden">
<h6 class="text-sm mt-3 mb-6 font-bold uppercase">{{'Filter'|trans}}</h6>
<div class="flex flex-wrap">
<div class="w-full lg:w-6/12 px-4">
{{ form_row(form.payment) }}
</div>
<div class="row">
<div class="col-md-4">
<div class="row mb-3">
<label class="col-md-3 form-control-label">{{ form_label(form.percent) }}</label>
<div class="col-md-9">
{{ form_widget(form.percent) }}
</div>
</div>
</div>
<div class="col-md-4">
<div class="row mb-3">
<label class="col-md-3 form-control-label">{{ form_label(form.value) }}</label>
<div class="col-md-9">
{{ form_widget(form.value) }}
</div>
</div>
</div>
<div class="col-md-4">
<div class="row mb-3">
<label class="col-md-3 form-control-label">{{ form_label(form.mode) }}</label>
<div class="col-md-9">
{{ form_widget(form.mode) }}
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-md-4">
<div class="row mb-3">
<label class="col-md-3 form-control-label">{{ form_label(form.more) }}</label>
<div class="col-md-9">
{{ form_widget(form.more) }}
</div>
</div>
</div>
<div class="col-md-4">
<div class="row mb-3">
<label class="col-md-3 form-control-label">{{ form_label(form.count) }}</label>
<div class="col-md-9">
{{ form_widget(form.count) }}
</div>
</div>
</div>
<div class="col-md-4">
<div class="row mb-3">
<label class="col-md-3 form-control-label">{{ form_label(form.code) }}</label>
<div class="col-md-9">
{{ form_widget(form.code) }}
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-md-4">
<div class="row mb-3">
<label class="col-md-3 form-control-label">{{ form_label(form.zeroPayment) }}</label>
<div class="col-md-9">
{{ form_widget(form.zeroPayment) }}
</div>
</div>
</div>
<div class="col-md-4">
<div class="row mb-3">
<label class="col-md-3 form-control-label">{{ form_label(form.zeroShipping) }}</label>
<div class="col-md-9">
{{ form_widget(form.zeroShipping) }}
</div>
</div>
</div>
<div class="col-md-4">
<div class="row mb-3">
<label class="col-md-3 form-control-label">{{ form_label(form.minBasketValue) }}</label>
<div class="col-md-9">
{{ form_widget(form.minBasketValue) }}
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-md-4">
<div class="row mb-3">
<label class="col-md-3 form-control-label">{{ form_label(form.payment) }}</label>
<div class="col-md-9">
{{ form_widget(form.payment) }}
</div>
</div>
</div>
<div class="col-md-4">
<div class="row mb-3">
<label class="col-md-3 form-control-label">{{ form_label(form.shipping) }}</label>
<div class="col-md-9">
{{ form_widget(form.shipping) }}
</div>
</div>
</div>
</div>
</div>
<div class="tab-pane" id="add" role="tabpanel">
<div class="row">
<div class="col-md-6">
<div class="row mb-3">
<label class="col-md-3 form-control-label">{{ form_label(form.productIds) }}</label>
<div class="col-md-9">
{{ form_widget(form.productIds) }}
</div>
</div>
</div>
<div class="col-md-6">
<div class="row mb-3">
<label class="col-md-3 form-control-label">{{ form_label(form.productGroupIds) }}</label>
<div class="col-md-9">
{{ form_widget(form.productGroupIds) }}
</div>
</div>
<div class="w-full lg:w-6/12 px-4">
{{ form_row(form.shipping) }}
</div>
</div>
<div class="flex flex-wrap">
<div class="w-full lg:w-6/12 px-4">
{{ form_row(form.productIds) }}
</div>
<div class="w-full lg:w-6/12 px-4">
{{ form_row(form.productGroupIds) }}
</div>
</div>
</div>
{# Custom Tabs #}
{% for customGroup in customGroups %}
<div class="tab-pane" id="{{ customGroup.id }}" role="tabpanel">
<div id="{{ customGroup.id }}" class="tab-content w-full text-stone-500 text-sm hidden">
<h6 class="text-sm mt-3 mb-6 font-bold uppercase">{{ customGroup.title }}</h6>
{% for customField in customFields %}
{% if customField.group == customGroup.id and customField.getTemplate %}
{{ include(customField.getTemplate, { 'form': form }) }}
@ -205,46 +170,19 @@
{% endfor %}
</div>
</div>
</div>
</div>
{# Save Button outside Card #}
<div class="text-end my-2">
{{ form_widget(form.save, {
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>
<div class="panel">
<div class="body">
<div class="row mb-3">
<div class="col-md-offset-1 col-md-11">
{{ form_widget(form.save, {attr: {class: 'btn btn-primary btn-sm'}}) }}
</div>
</div>
</div>
</div>
{{ form_end(form) }}
</div>
</div>
{{ summernote_mediabundle_init('default') }}
<div class="panel">
<div class="header">
<h4>{{ 'Changes'|trans }}</h4>
</div>
<div class="body">
<table class="table">
<thead>
<tr><th>{{ 'Date'|trans }}</th><th>{{ 'Username'|trans }}</th><th>{{'Changes'|trans}}</th></tr>
</thead>
<tbody>
{% for change in changes %}
<tr><td>{{ change.created|date('H:i:s d.m.Y') }}</td><td>{{ change.username }}</td><td>
{% for key,set in change.changeset %}
{% if set[1] is not iterable %}
<strong>{{key}}</strong> <span class="badge bg-danger"><del>{{ set[0]}}</del></span><span class="badge bg-success">{% if set[1] is null %}0{% else %}{{ set[1] }}{% endif %}</span></br>
{% endif %}
{% endfor %}
<td></tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{{ summernote_mediabundle_init('default') }}
{% endblock %}

View File

@ -1,75 +1,145 @@
{% extends 'backend_base.html.twig' %}
{% extends 'backend_tailwind_base.html.twig' %}
{% trans_default_domain 'core_voucher_list' %}
{% 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="M16.5 6v.75m0 3v.75m0 3v.75m0 3V18m-9-5.25h5.25M7.5 15h3M3.375 5.25c-.621 0-1.125.504-1.125 1.125v3.026a2.999 2.999 0 010 5.198v3.026c0 .621.504 1.125 1.125 1.125h17.25c.621 0 1.125-.504 1.125-1.125v-3.026a2.999 2.999 0 010-5.198V6.375c0-.621-.504-1.125-1.125-1.125H3.375z" />
</svg>
{{'Voucher'|trans}} <span class="text-gray-500">{{'List'|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_create') }}" 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]">
<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="M12 4.5v15m7.5-7.5h-15" />
</svg>
{{'Addvoucher'|trans}}
</a>
</div>
</div>
{% endblock %}
{% block body %}
<div class="header">
<div class="row">
<div class="col-xs-12 col-sm-6 col-md-6 col-lg-6">
<h3>
<i class="fa-fw fa fa-id-card"></i>
{{'Voucher'|trans}} <span>>
{{'List'|trans}} </span>
</h3>
</div>
</div>
</div>
<div class="body">
<div class="panel">
<div class="body">
<a class="btn btn-info btn-sm" href="{{ path("psc_shop_voucher_backend_create") }}">{{'Addvoucher'|trans}}</a>
</div>
</div>
<div class="panel">
<div class="body">
<table class="table">
<thead class="thead-dark">
<tr>
<th>{{ knp_pagination_sortable(pagination, 'Uid'|trans, 'voucher.uid') }}</th>
<th>{{ knp_pagination_sortable(pagination, 'Createdat'|trans, 'product.createdAt') }}</th>
<th>{{ knp_pagination_sortable(pagination, 'Updatedat'|trans, 'product.updatedAt') }}</th>
<th>{{ knp_pagination_sortable(pagination, 'active'|trans, 'voucher.enable') }}</th>
<th>{{ knp_pagination_sortable(pagination, 'Name'|trans, 'voucher.title') }}</th>
<th>{{ knp_pagination_sortable(pagination, 'from'|trans, 'voucher.fromDate') }}</th>
<th>{{ knp_pagination_sortable(pagination, 'to'|trans, 'voucher.toDate') }}</th>
<th>{{ knp_pagination_sortable(pagination, 'Percent'|trans, 'voucher.percent') }}</th>
<th>{{ knp_pagination_sortable(pagination, 'Value'|trans, 'voucher.value') }}</th>
<th>{{'Numbertogenerate'|trans}}</th>
<th>{{'Numbergenerated'|trans}}</th>
<th></th>
<div class="flex flex-col gap-6">
<div class="rounded-md border bg-white px-7.5 py-6 shadow-lg dark:border-strokedark dark:bg-boxdark">
<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">
{{ knp_pagination_sortable(pagination, 'Uid'|trans, 'voucher.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, 'Createdat'|trans, 'product.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, 'Updatedat'|trans, 'product.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, 'active'|trans, 'voucher.enable') }}
</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, 'Name'|trans, 'voucher.title') }}
</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, 'from'|trans, 'voucher.fromDate') }}
</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, 'to'|trans, 'voucher.toDate') }}
</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, 'Percent'|trans, 'voucher.percent') }}
</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, 'Value'|trans, 'voucher.value') }}
</th>
<th class="px-4 py-4 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider dark:text-gray-300">
{{'Numbertogenerate'|trans}}
</th>
<th class="px-4 py-4 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider dark:text-gray-300">
{{'Numbergenerated'|trans}}
</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>
</thead>
<tbody>
<tbody class="bg-white dark:bg-boxdark">
{% for voucher in pagination %}
<tr {% if loop.index is odd %}class="color"{% endif %}>
<td>{{ voucher.uid }}</td>
<td>{{ voucher.createdAt|date('H:i d.m.Y') }}</td>
<td>{{ voucher.updatedAt|date('H:i d.m.Y') }}</td>
<td>{% if voucher.enable %}<span class="badge bg-success">{{'yes'|trans}}</span>{% else %}<span
class="badge bg-warning">{{'no'|trans}}</span>{% endif %}</td>
<td>{{ voucher.title }}</td>
<td>{{ voucher.fromDate|date('d.m.Y') }}</td>
<td>{{ voucher.toDate|date('d.m.Y') }}</td>
<td>{% if voucher.percent %}<span class="badge bg-success">{{'yes'|trans}}</span>{% else %}<span
class="badge bg-warning">{{'no'|trans}}</span>{% endif %}</td>
<td>{{ voucher.value|number_format(2, ',', '.') }}</td>
<td>{% if voucher.more %}Keine Individuellen Codes{% else %}{{ voucher.count }}{% endif %}</td>
<td>{% if voucher.more %}Keine Individuellen Codes{% else %}{{ voucher.voucherItems|length }}{% endif %}</td>
<td class="text-end">
<a href="{{ path('psc_shop_voucher_backend_export', {uid: voucher.uid}) }}" target="_blank" class="btn btn-info btn-sm" style="color: white !important;">{{'Export'|trans}}</a>
<a href="{{ path("psc_shop_voucher_backend_generate", {uid: voucher.uid}) }}"
class="btn btn-warning btn-sm"><span class="fa fa-trash"></span> {{'Generate'|trans}}</a>
<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-4 py-3">
<a href="{{ path("psc_shop_voucher_backend_edit", {uid: voucher.uid}) }}"
class="btn btn-info btn-sm"><span class="fa fa-edit"></span></a>
<a href="{{ path("psc_shop_voucher_backend_delete", {uid: voucher.uid}) }}"
class="btn btn-danger btn-sm"><span class="fa fa-trash"></span></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>
{{ voucher.uid }}
</a>
</td>
<td class="px-4 py-3 text-gray-900 dark:text-gray-100">{{ voucher.createdAt|date('H:i d.m.Y') }}</td>
<td class="px-4 py-3 text-gray-900 dark:text-gray-100">{{ voucher.updatedAt|date('H:i d.m.Y') }}</td>
<td class="px-4 py-3">
{% if voucher.enable %}
<span class="badge-yes">{{'yes'|trans}}</span>
{% else %}
<span class="badge-no">{{'no'|trans}}</span>
{% endif %}
</td>
<td class="px-4 py-3 font-medium text-gray-900 dark:text-gray-100">{{ voucher.title }}</td>
<td class="px-4 py-3 text-gray-900 dark:text-gray-100">{{ voucher.fromDate|date('d.m.Y') }}</td>
<td class="px-4 py-3 text-gray-900 dark:text-gray-100">{{ voucher.toDate|date('d.m.Y') }}</td>
<td class="px-4 py-3">
{% if voucher.percent %}
<span class="badge-yes">{{'yes'|trans}}</span>
{% else %}
<span class="badge-no">{{'no'|trans}}</span>
{% endif %}
</td>
<td class="px-4 py-3 text-gray-900 dark:text-gray-100">{{ voucher.value|number_format(2, ',', '.') }}</td>
<td class="px-4 py-3 text-gray-900 dark:text-gray-100">{% if voucher.more %}Keine Individuellen Codes{% else %}{{ voucher.count }}{% 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">
<div class="flex flex-row gap-2 justify-end">
{# 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">
<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.5M16.5 12L12 16.5m0 0L7.5 12m4.5 4.5V3" />
</svg>
</a>
{# Generate - GELB #}
<a href="{{ path("psc_shop_voucher_backend_generate", {uid: voucher.uid}) }}" title="{{'Generate'|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-yellow-600 hover:text-yellow-700 dark:text-yellow-500 dark:hover:text-yellow-400">
<path stroke-linecap="round" stroke-linejoin="round" d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0l3.181 3.183a8.25 8.25 0 0013.803-3.7M4.031 9.865a8.25 8.25 0 0113.803-3.7l3.181 3.182m0-4.991v4.99" />
</svg>
</a>
{# Bearbeiten - GRÜN #}
<a href="{{ path("psc_shop_voucher_backend_edit", {uid: voucher.uid}) }}" 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" />
</svg>
</a>
{# Löschen - ROT #}
<a href="{{ path("psc_shop_voucher_backend_delete", {uid: voucher.uid}) }}" title="{{'delete'|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-red-600 hover:text-red-700 dark:text-red-500 dark:hover:text-red-400">
<path stroke-linecap="round" stroke-linejoin="round" d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0" />
</svg>
</a>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
<div class="navigation">
{{ knp_pagination_render(pagination) }}
</div>
</div>
</div>
<div class="mt-4">
{{ knp_pagination_render(pagination, 'tailwind_pagination.html.twig', {}, {
'sortableTemplate': 'tailwind_sortable.html.twig'
}) }}
</div>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,150 @@
<?php
namespace Plugin\Custom\PSC\Gutschein\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Table(name="gutschein_codes")
* @ORM\Entity(repositoryClass="Plugin\Custom\PSC\Gutschein\Repository\GutscheinCodeRepository")
*/
class GutscheinCode
{
/**
* @ORM\Column(name="id", type="integer")
* @ORM\Id
* @ORM\GeneratedValue(strategy="IDENTITY")
*/
private ?int $id = null;
/**
* @ORM\Column(name="code", type="string", length=30, unique=true)
*/
private string $code;
/**
* @ORM\Column(name="order_position_id", type="integer")
*/
private int $orderPositionId;
/**
* @ORM\Column(name="amount", type="decimal", precision=10, scale=2)
*/
private float $amount;
/**
* @ORM\Column(name="expiration_date", type="datetime")
*/
private \DateTime $expirationDate;
/**
* @ORM\Column(name="created_at", type="datetime")
*/
private \DateTime $createdAt;
/**
* @ORM\Column(name="used", type="boolean")
*/
private bool $used = false;
/**
* @ORM\Column(name="used_at", type="datetime", nullable=true)
*/
private ?\DateTime $usedAt = null;
/**
* @ORM\Column(name="used_in_order_id", type="integer", nullable=true)
*/
private ?int $usedInOrderId = null;
// Getters and Setters
public function getId(): ?int
{
return $this->id;
}
public function getCode(): string
{
return $this->code;
}
public function setCode(string $code): void
{
$this->code = $code;
}
public function getOrderPositionId(): int
{
return $this->orderPositionId;
}
public function setOrderPositionId(int $orderPositionId): void
{
$this->orderPositionId = $orderPositionId;
}
public function getAmount(): float
{
return $this->amount;
}
public function setAmount(float $amount): void
{
$this->amount = $amount;
}
public function getExpirationDate(): \DateTime
{
return $this->expirationDate;
}
public function setExpirationDate(\DateTime $expirationDate): void
{
$this->expirationDate = $expirationDate;
}
public function getCreatedAt(): \DateTime
{
return $this->createdAt;
}
public function setCreatedAt(\DateTime $createdAt): void
{
$this->createdAt = $createdAt;
}
public function isUsed(): bool
{
return $this->used;
}
public function setUsed(bool $used): void
{
$this->used = $used;
}
public function getUsedAt(): ?\DateTime
{
return $this->usedAt;
}
public function setUsedAt(?\DateTime $usedAt): void
{
$this->usedAt = $usedAt;
}
public function getUsedInOrderId(): ?int
{
return $this->usedInOrderId;
}
public function setUsedInOrderId(?int $usedInOrderId): void
{
$this->usedInOrderId = $usedInOrderId;
}
public function isValid(): bool
{
return !$this->used && $this->expirationDate > new \DateTime();
}
}

View File

@ -0,0 +1,87 @@
<?php
namespace Plugin\Custom\PSC\Gutschein\Form\Field;
use PSC\Shop\EntityBundle\Entity\Product;
use PSC\System\PluginBundle\Form\Interfaces\Field;
use PSC\System\SettingsBundle\Service\General;
use PSC\System\SettingsBundle\Service\Tax;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\IntegerType;
use Symfony\Component\Form\Extension\Core\Type\MoneyType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormEvent;
class Calc implements Field
{
public function __construct(
protected General $generalService,
protected Tax $taxService,
) {}
public function getTemplate()
{
return '@PluginCustomPSCGutschein/form/field/calc.html.twig';
}
public function getModule()
{
return Field::Product;
}
public function formPreSubmit(FormEvent $event)
{
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
$product = $options['product'];
$builder->add('minAmount', MoneyType::class, [
'required' => false,
'label' => 'Minimaler Gutscheinwert',
'data' => 10.00,
])->add('maxAmount', MoneyType::class, [
'required' => false,
'label' => 'Maximaler Gutscheinwert',
'data' => 500.00,
])->add('defaultAmount', MoneyType::class, [
'required' => false,
'label' => 'Voreingestellter Wert',
'data' => 50.00,
])->add('validityMonths', IntegerType::class, [
'required' => false,
'label' => 'Gültigkeit (Monate)',
'data' => 12,
]);
if ($product) {
$builder->get('defaultAmount')->setData($product->gutschein['defaultAmount'] ?? null);
$builder->get('minAmount')->setData($product->gutschein['minAmount'] ?? null);
$builder->get('maxAmount')->setData($product->gutschein['maxAmount'] ?? null);
$builder->get('validityMonths')->setData($product->gutschein['validityMonths'] ?? null);
}
return $builder;
}
public function getGroup()
{
return \Plugin\Custom\PSC\Gutschein\Form\Group\Calc::GROUP_ID;
}
public function formPostSetData(FormEvent $event)
{
}
public function formPostSubmit(FormEvent $event)
{
}
public function formPreSetData(FormEvent $event)
{
}
public function formSubmit(FormEvent $event)
{
}
}

View File

@ -0,0 +1,26 @@
<?php
namespace Plugin\Custom\PSC\Gutschein\Form\Group;
use PSC\System\PluginBundle\Form\Group;
use PSC\System\PluginBundle\Form\Interfaces\Field;
class Calc extends Group
{
const GROUP_ID = 'gutschein';
public function __construct()
{
$this->title = 'Gutschein-Einstellungen';
}
public function getModule()
{
return Field::Product;
}
public function getId()
{
return self::GROUP_ID;
}
}

View File

@ -0,0 +1,172 @@
<?php
namespace Plugin\Custom\PSC\Gutschein\Model;
use PSC\Shop\OrderBundle\Model\Order\Position\IProductTypeObject;
class ProductSpecialObject implements IProductTypeObject
{
private int $taxClass = 1900; // 19% VAT
private array $params = [];
// Backend configuration (set by admin)
private float $minAmount = 10.00;
private float $maxAmount = 500.00;
private ?float $defaultAmount = 50.00;
private int $validityMonths = 12;
// Customer-entered data
private float $voucherAmount = 0;
private string $recipientName = '';
private string $greetingMessage = '';
private string $deliveryMethod = 'digital';
// Generated data
private string $voucherCode = '';
private ?\DateTime $expirationDate = null;
public function getName(): string
{
return 'Gutscheinprodukt';
}
public function getTyp(): int
{
return 9; // Product Type ID
}
public function getPositionData(): array
{
return [
'params' => $this->params,
'voucherAmount' => $this->voucherAmount,
'recipientName' => $this->recipientName,
'greetingMessage' => $this->greetingMessage,
'deliveryMethod' => $this->deliveryMethod,
'voucherCode' => $this->voucherCode,
'expirationDate' => $this->expirationDate?->format('Y-m-d H:i:s'),
'validityMonths' => $this->validityMonths,
];
}
// Getters and Setters
public function getTaxClass(): int
{
return $this->taxClass;
}
public function setTaxClass(int $taxClass): void
{
$this->taxClass = $taxClass;
}
public function getParams(): array
{
return $this->params;
}
public function setParams(array $params): void
{
$this->params = $params;
}
public function getMinAmount(): float
{
return $this->minAmount;
}
public function setMinAmount(float $minAmount): void
{
$this->minAmount = $minAmount;
}
public function getMaxAmount(): float
{
return $this->maxAmount;
}
public function setMaxAmount(float $maxAmount): void
{
$this->maxAmount = $maxAmount;
}
public function getDefaultAmount(): ?float
{
return $this->defaultAmount;
}
public function setDefaultAmount(?float $defaultAmount): void
{
$this->defaultAmount = $defaultAmount;
}
public function getValidityMonths(): int
{
return $this->validityMonths;
}
public function setValidityMonths(int $validityMonths): void
{
$this->validityMonths = $validityMonths;
}
public function getVoucherAmount(): float
{
return $this->voucherAmount;
}
public function setVoucherAmount(float $voucherAmount): void
{
$this->voucherAmount = $voucherAmount;
}
public function getRecipientName(): string
{
return $this->recipientName;
}
public function setRecipientName(string $recipientName): void
{
$this->recipientName = $recipientName;
}
public function getGreetingMessage(): string
{
return $this->greetingMessage;
}
public function setGreetingMessage(string $greetingMessage): void
{
$this->greetingMessage = $greetingMessage;
}
public function getDeliveryMethod(): string
{
return $this->deliveryMethod;
}
public function setDeliveryMethod(string $deliveryMethod): void
{
$this->deliveryMethod = $deliveryMethod;
}
public function getVoucherCode(): string
{
return $this->voucherCode;
}
public function setVoucherCode(string $voucherCode): void
{
$this->voucherCode = $voucherCode;
}
public function getExpirationDate(): ?\DateTime
{
return $this->expirationDate;
}
public function setExpirationDate(?\DateTime $expirationDate): void
{
$this->expirationDate = $expirationDate;
}
}

View File

@ -0,0 +1,40 @@
<?php
namespace Plugin\Custom\PSC\Gutschein;
use PSC\System\PluginBundle\Plugin\Base;
class Plugin extends Base implements \PSC\System\PluginBundle\Interfaces\Plugin
{
protected $name = 'Gutschein Produkt';
public function getType()
{
return self::ProductType;
}
public function getDescription(): string
{
return 'Gutscheine mit variablem Wert, Personalisierung und Gültigkeitsdauer';
}
public function getVersion(): string
{
return 1;
}
public function getIdentifier(): string
{
return 'gutschein';
}
public function getInstallClass(): string
{
return '';
}
public function getDeInstallClass(): string
{
return '';
}
}

View File

@ -0,0 +1,202 @@
<?php
namespace Plugin\Custom\PSC\Gutschein\Producer;
use Brick\Math\RoundingMode;
use Brick\Money\Money;
use Doctrine\ORM\EntityManagerInterface;
use Plugin\Custom\PSC\Gutschein\Model\ProductSpecialObject;
use Plugin\Custom\PSC\Gutschein\Service\CodeGenerator;
use PSC\Shop\OrderBundle\Model\Order\Position;
use PSC\Shop\OrderBundle\Model\Order\Position\Price;
use PSC\Shop\OrderBundle\Model\Order\Tax;
use PSC\Shop\OrderBundle\Model\Order\TaxEnum;
use PSC\Shop\ProductBundle\Interfaces\IProducer;
use PSC\Shop\ProductBundle\Interfaces\IProducerHydrateModel;
use PSC\Shop\ProductBundle\Interfaces\IProductTransformer;
use PSC\Shop\ProductBundle\Interfaces\IUiProducer;
use PSC\Shop\ProductBundle\Model\Product;
class Producer implements IUiProducer, IProducerHydrateModel
{
private Product $product;
private float $voucherAmount = 0; // Customer-entered amount
private int $count = 1;
private string $recipientName = '';
private string $greetingMessage = '';
private string $deliveryMethod = 'digital'; // 'digital' or 'physical'
public function __construct(
private readonly EntityManagerInterface $entityManager,
private readonly CodeGenerator $codeGenerator,
) {}
public function setProduct(Product $product): void
{
$this->product = $product;
$this->process();
}
public function getPrice(): Price
{
/** @var ProductSpecialObject $specProd */
$specProd = $this->product->getSpecialProductTypeObject();
// CRITICAL: Voucher amount is customer-defined, not from product entity
$priceObj = Money::ofMinor($this->voucherAmount * 100, 'EUR');
$price = new Price();
$price->setNet($priceObj->getMinorAmount()->toInt());
$price->setVat(
$priceObj
->toRational()
->dividedBy(100)
->multipliedBy($specProd->getTaxClass() / 100)
->to($priceObj->getContext(), RoundingMode::UP)
->getMinorAmount()
->toInt(),
);
$price->setGross($price->getNet() + $price->getVat());
$price->setCount($this->count);
$price->setAllNet($price->getNet() * $this->count);
$price->setAllVat($price->getVat() * $this->count);
$price->setAllGross($price->getGross() * $this->count);
$price->tax = new Tax($specProd->getTaxClass(), $price->getAllNet(), $price->getAllVat(), TaxEnum::POSITION);
return $price;
}
public function getJsonForm(): array
{
/** @var ProductSpecialObject $specProd */
$specProd = $this->product->getSpecialProductTypeObject();
return [
'title' => $this->product->getTitle(),
'type' => 'object',
'properties' => [
'voucherAmount' => [
'type' => 'number',
'title' => 'Gutscheinwert (€)',
'minimum' => $specProd->getMinAmount(),
'maximum' => $specProd->getMaxAmount(),
'default' => $specProd->getDefaultAmount() ?? 50.00,
],
'recipientName' => [
'type' => 'string',
'title' => 'Name des Empfängers',
'maxLength' => 100,
],
'greetingMessage' => [
'type' => 'string',
'title' => 'Grußtext (optional)',
'maxLength' => 500,
],
'deliveryMethod' => [
'type' => 'string',
'title' => 'Versandart',
'enum' => ['digital', 'physical'],
'enumNames' => ['Digital (E-Mail)', 'Gedruckt (Postversand)'],
'default' => 'digital',
],
'count' => [
'type' => 'integer',
'title' => 'Anzahl',
'minimum' => 1,
'default' => 1,
],
],
'required' => ['voucherAmount', 'recipientName', 'deliveryMethod'],
];
}
public function setParams(array $params): void
{
if (isset($params['voucherAmount'])) {
$this->voucherAmount = (float) $params['voucherAmount'];
}
if (isset($params['recipientName'])) {
$this->recipientName = $params['recipientName'];
}
if (isset($params['greetingMessage'])) {
$this->greetingMessage = $params['greetingMessage'];
}
if (isset($params['deliveryMethod'])) {
$this->deliveryMethod = $params['deliveryMethod'];
}
if (isset($params['count'])) {
$this->count = (int) $params['count'];
}
}
public function getCount(): int
{
return $this->count;
}
public function calcPriceForOrderPosition(Position $position): void
{
// Generate unique voucher code for this order position
$voucherCode = $this->codeGenerator->generate();
/** @var ProductSpecialObject $specProd */
$specProd = $position->getProduct()->getSpecialProductTypeObject();
$specProd->setVoucherCode($voucherCode);
$specProd->setVoucherAmount($this->voucherAmount);
$specProd->setRecipientName($this->recipientName);
$specProd->setGreetingMessage($this->greetingMessage);
$specProd->setDeliveryMethod($this->deliveryMethod);
// Calculate expiration date
$validityMonths = $specProd->getValidityMonths() ?? 12;
$expirationDate = new \DateTime();
$expirationDate->modify("+{$validityMonths} months");
$specProd->setExpirationDate($expirationDate);
$position->setPrice($this->getPrice());
}
public function getProductTransformer(): IProductTransformer
{
return new \Plugin\Custom\PSC\Gutschein\Transformer\Product();
}
public function getUiJsonForm(): array
{
/** @var ProductSpecialObject $specProd */
$specProd = $this->product->getSpecialProductTypeObject();
$minAmount = $specProd->getMinAmount();
$maxAmount = $specProd->getMaxAmount();
return [
'ui:submitButtonOptions' => [
'submitText' => 'In den Warenkorb',
'norender' => true,
'props' => [
'disabled' => false,
'className' => 'btn btn-primary btn-lg',
],
],
'voucherAmount' => [
'ui:widget' => 'updown',
'ui:help' => "Wählen Sie einen Betrag zwischen {$minAmount}€ und {$maxAmount}",
],
'greetingMessage' => [
'ui:widget' => 'textarea',
'ui:options' => [
'rows' => 5,
],
],
];
}
private function process(): void
{
// Load existing params from product if available
if (!empty($this->product->getSpecialProductTypeObject()->getParams())) {
$params = $this->product->getSpecialProductTypeObject()->getParams();
$this->setParams($params);
}
}
}

View File

@ -0,0 +1,61 @@
<?php
namespace Plugin\Custom\PSC\Gutschein\Repository;
use Doctrine\ORM\EntityRepository;
use Plugin\Custom\PSC\Gutschein\Entity\GutscheinCode;
class GutscheinCodeRepository extends EntityRepository
{
public function findValidCode(string $code): ?GutscheinCode
{
$qb = $this->createQueryBuilder('gc');
return $qb
->where('gc.code = :code')
->andWhere('gc.used = :used')
->andWhere('gc.expirationDate > :now')
->setParameter('code', $code)
->setParameter('used', false)
->setParameter('now', new \DateTime())
->getQuery()
->getOneOrNullResult();
}
public function findByOrderPosition(int $orderPositionId): array
{
return $this->findBy(['orderPositionId' => $orderPositionId]);
}
public function getUsageStats(): array
{
$qb = $this->createQueryBuilder('gc');
$total = (clone $qb)
->select('COUNT(gc.id)')
->getQuery()
->getSingleScalarResult();
$used = (clone $qb)
->select('COUNT(gc.id)')
->where('gc.used = :used')
->setParameter('used', true)
->getQuery()
->getSingleScalarResult();
$expired = (clone $qb)
->select('COUNT(gc.id)')
->where('gc.expirationDate < :now')
->andWhere('gc.used = :used')
->setParameter('now', new \DateTime())
->setParameter('used', false)
->getQuery()
->getSingleScalarResult();
return [
'total' => $total,
'used' => $used,
'expired' => $expired,
];
}
}

View File

@ -0,0 +1,19 @@
services:
_defaults:
autowire: true
autoconfigure: true
Plugin\Custom\PSC\Gutschein\:
resource: '../../*/*'
Plugin\Custom\PSC\Gutschein\Service\ProductType:
tags:
- { name: psc.product.type }
Plugin\Custom\PSC\Gutschein\Form\Group\Calc:
tags:
- { name: psc.backend.custom.groups, productType: 9 }
Plugin\Custom\PSC\Gutschein\Form\Field\Calc:
tags:
- { name: psc.backend.custom.fields, productType: 9 }

View File

@ -0,0 +1,44 @@
<div class="card mb-3">
<div class="card-header">
<h5>Gutschein-Konfiguration</h5>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-6">
<div class="form-group">
{{ form_label(form.gutschein.validityMonths) }}
{{ form_widget(form.gutschein.validityMonths) }}
<small class="form-text text-muted">Gültigkeitsdauer ab Kaufdatum</small>
</div>
</div>
</div>
<div class="row">
<div class="col-md-4">
<div class="form-group">
{{ form_label(form.gutschein.minAmount) }}
{{ form_widget(form.gutschein.minAmount) }}
<small class="form-text text-muted">Mindestbetrag</small>
</div>
</div>
<div class="col-md-4">
<div class="form-group">
{{ form_label(form.gutschein.defaultAmount) }}
{{ form_widget(form.gutschein.defaultAmount) }}
<small class="form-text text-muted">Vorauswahl</small>
</div>
</div>
<div class="col-md-4">
<div class="form-group">
{{ form_label(form.gutschein.maxAmount) }}
{{ form_widget(form.gutschein.maxAmount) }}
<small class="form-text text-muted">Maximalbetrag</small>
</div>
</div>
</div>
<div class="alert alert-info">
<strong>Hinweis:</strong> Kunden können beim Kauf einen beliebigen Betrag zwischen Minimal- und Maximalbetrag wählen.
</div>
</div>
</div>

View File

@ -0,0 +1,83 @@
<?php
namespace Plugin\Custom\PSC\Gutschein\Service;
use Doctrine\ORM\EntityManagerInterface;
use Plugin\Custom\PSC\Gutschein\Entity\GutscheinCode;
class CodeGenerator
{
private const CODE_LENGTH = 16;
private const CODE_PREFIX = 'GS';
private const MAX_ATTEMPTS = 10;
public function __construct(
private readonly EntityManagerInterface $entityManager
) {
}
public function generate(): string
{
$attempts = 0;
do {
$code = $this->generateCode();
$exists = $this->codeExists($code);
$attempts++;
if ($attempts >= self::MAX_ATTEMPTS) {
throw new \RuntimeException('Could not generate unique voucher code after ' . self::MAX_ATTEMPTS . ' attempts');
}
} while ($exists);
return $code;
}
private function generateCode(): string
{
// Format: GS-XXXX-XXXX-XXXX-XXXX
$segments = [];
for ($i = 0; $i < 4; $i++) {
$segments[] = $this->randomSegment(4);
}
return self::CODE_PREFIX . '-' . implode('-', $segments);
}
private function randomSegment(int $length): string
{
$characters = '0123456789ABCDEFGHJKLMNPQRSTUVWXYZ'; // Exclude I, O to avoid confusion
$segment = '';
for ($i = 0; $i < $length; $i++) {
$segment .= $characters[random_int(0, strlen($characters) - 1)];
}
return $segment;
}
private function codeExists(string $code): bool
{
$existing = $this->entityManager
->getRepository(GutscheinCode::class)
->findOneBy(['code' => $code]);
return $existing !== null;
}
public function saveCode(string $code, int $orderPositionId, float $amount, \DateTime $expirationDate): GutscheinCode
{
$gutscheinCode = new GutscheinCode();
$gutscheinCode->setCode($code);
$gutscheinCode->setOrderPositionId($orderPositionId);
$gutscheinCode->setAmount($amount);
$gutscheinCode->setExpirationDate($expirationDate);
$gutscheinCode->setCreatedAt(new \DateTime());
$gutscheinCode->setUsed(false);
$this->entityManager->persist($gutscheinCode);
$this->entityManager->flush();
return $gutscheinCode;
}
}

View File

@ -0,0 +1,47 @@
<?php
namespace Plugin\Custom\PSC\Gutschein\Service;
use Plugin\Custom\PSC\Gutschein\Model\ProductSpecialObject;
use Plugin\Custom\PSC\Gutschein\Producer\Producer;
use Plugin\Custom\PSC\Gutschein\Transformer\Position;
use PSC\Shop\OrderBundle\Model\Order\Position\IProductTypeObject;
use PSC\Shop\OrderBundle\Transformer\Order\Position\IPositionTransformer;
use PSC\Shop\ProductBundle\Interfaces\IProducer;
class ProductType implements \PSC\System\PluginBundle\Product\Type
{
private Position $productTransformer;
private Producer $producer;
public function __construct(Position $positionTransformer, Producer $producer)
{
$this->productTransformer = $positionTransformer;
$this->producer = $producer;
}
public function getId()
{
return 9; // NEW UNIQUE ID
}
public function getName()
{
return 'Gutscheinprodukt';
}
public function getPositionProductTransformer(): IPositionTransformer
{
return $this->productTransformer;
}
public function getProducer(): IProducer
{
return $this->producer;
}
public function getProductTypeObject(): IProductTypeObject
{
return new ProductSpecialObject();
}
}

View File

@ -0,0 +1,56 @@
<?php
namespace Plugin\Custom\PSC\Gutschein\Transformer;
use PSC\Shop\EntityBundle\Entity\Orderpos;
use PSC\Shop\OrderBundle\Transformer\Order\Position\IPositionTransformer;
use Plugin\Custom\PSC\Gutschein\Model\ProductSpecialObject;
class Position implements IPositionTransformer
{
public function fromDb(
\PSC\Shop\OrderBundle\Model\Order\Position $position,
Orderpos $posEntity,
\PSC\Shop\EntityBundle\Document\Position $posDoc
) {
$obj = new ProductSpecialObject();
$data = $posDoc->getSpecialProductTypeObject();
if (isset($data['params'])) {
$obj->setParams($data['params']);
}
if (isset($data['voucherAmount'])) {
$obj->setVoucherAmount($data['voucherAmount']);
}
if (isset($data['recipientName'])) {
$obj->setRecipientName($data['recipientName']);
}
if (isset($data['greetingMessage'])) {
$obj->setGreetingMessage($data['greetingMessage']);
}
if (isset($data['deliveryMethod'])) {
$obj->setDeliveryMethod($data['deliveryMethod']);
}
if (isset($data['voucherCode'])) {
$obj->setVoucherCode($data['voucherCode']);
}
if (isset($data['expirationDate'])) {
$obj->setExpirationDate(new \DateTime($data['expirationDate']));
}
if (isset($data['validityMonths'])) {
$obj->setValidityMonths($data['validityMonths']);
}
$position->getProduct()->setSpecialProductTypeObject($obj);
}
public function toDb(
\PSC\Shop\OrderBundle\Model\Order\Position $position,
Orderpos $posEntity,
\PSC\Shop\EntityBundle\Document\Position $posDoc
) {
// Position data is stored via getPositionData() in ProductSpecialObject
// No additional transformation needed here
}
}

View File

@ -0,0 +1,54 @@
<?php
namespace Plugin\Custom\PSC\Gutschein\Transformer;
use Plugin\Custom\PSC\Gutschein\Model\ProductSpecialObject;
use PSC\Shop\ProductBundle\Interfaces\IProductTransformer;
class Product implements IProductTransformer
{
public function fromDb(
\PSC\Shop\ProductBundle\Model\Product $productModel,
\PSC\Shop\EntityBundle\Entity\Product $productEntity,
\PSC\Shop\EntityBundle\Document\Product $productDoc
): void {
if ($productModel->getSpecialProductTypeObject() == null) {
$productModel->setSpecialProductTypeObject(new ProductSpecialObject());
}
/** @var ProductSpecialObject $prodSpec */
$prodSpec = $productModel->getSpecialProductTypeObject();
// Load backend configuration from product entity
$prodSpec->setTaxClass($productEntity->getMwert() * 100);
// Load custom fields from MongoDB document (if stored there)
if ($productDoc->getCustom1()) {
$prodSpec->setMinAmount((float)$productDoc->getCustom1());
}
if ($productDoc->getCustom2()) {
$prodSpec->setMaxAmount((float)$productDoc->getCustom2());
}
if ($productDoc->getCustom3()) {
$prodSpec->setDefaultAmount((float)$productDoc->getCustom3());
}
if ($productDoc->getCustom4()) {
$prodSpec->setValidityMonths((int)$productDoc->getCustom4());
}
}
public function toDb(
\PSC\Shop\ProductBundle\Model\Product $productModel,
\PSC\Shop\EntityBundle\Entity\Product $productEntity,
\PSC\Shop\EntityBundle\Document\Product $productDoc
): void {
/** @var ProductSpecialObject $prodSpec */
$prodSpec = $productModel->getSpecialProductTypeObject();
// Save configuration to MongoDB custom fields
$productDoc->setCustom1((string)$prodSpec->getMinAmount());
$productDoc->setCustom2((string)$prodSpec->getMaxAmount());
$productDoc->setCustom3((string)$prodSpec->getDefaultAmount());
$productDoc->setCustom4((string)$prodSpec->getValidityMonths());
}
}

View File

@ -67,6 +67,7 @@
</div>
</div>
<div class="mt-6 text-right">
<a class="bg-yellow-500 hover:bg-gray-100 text-gray-800 font-semibold py-2 px-4 rounded shadow" href="/article/show/uuid/{{ product.uuid }}">Abbrechen & Zurück</a>
<button class="px-6 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 disabled:bg-gray-300 disabled:cursor-not-allowed" id="nextToStep2" disabled>Weiter zu Schritt 2</button>
</div>
</div>
@ -799,7 +800,19 @@
title="Berechneter Wert: ${tabInfo}"
onchange="updateTabNumber(${index}, this.value)" />
</td>
<td class="px-4 py-3 whitespace-nowrap text-center">
<td class="px-4 py-3 whitespace-nowrap text-center" onclick="event.stopPropagation()">
<select class="px-2 py-1 border border-gray-300 rounded focus:outline-none focus:border-blue-500"
id="color-select-${index}"
onchange="updateColor(${index}, this.value)"
style="color: ${pdf.color || 'black'};">
<option value="black" ${(!pdf.color || pdf.color === 'black') ? 'selected' : ''} style="color: black;">Schwarz</option>
<option value="red" ${pdf.color === 'red' ? 'selected' : ''} style="color: red;">Rot</option>
<option value="green" ${pdf.color === 'green' ? 'selected' : ''} style="color: green;">Grün</option>
<option value="blue" ${pdf.color === 'blue' ? 'selected' : ''} style="color: blue;">Blau</option>
<option value="yellow" ${pdf.color === 'yellow' ? 'selected' : ''} style="color: #997700;">Gelb</option>
</select>
</td>
<td class="px-4 py-3 whitespace-nowrap text-center">
${ocrPreviewHtml}
</td>
<td class="px-4 py-3 whitespace-nowrap text-center" onclick="event.stopPropagation()">

View File

@ -5,15 +5,14 @@ namespace Plugin\Custom\PSC\Pitchprint\Form\Field;
use Plugin\Custom\PSC\Pitchprint\Form\Group\Pitchprint;
use PSC\Shop\EntityBundle\Document\Shop;
use PSC\System\PluginBundle\Form\Interfaces\Field;
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\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormEvent;
class ShopSettings implements Field
{
public function getTemplate()
{
return '@PluginCustomPSCPitchprint/form/field/ShopSettings.html.twig';
@ -29,15 +28,22 @@ class ShopSettings implements Field
*/
public function formPostSubmit(FormEvent $event)
{
}
public function formPostSetData(FormEvent $event)
{
/** @var Shop $data */
$data = $event->getData();
$event->getForm()->get('Pitchprint')->get('publicKey')->setData($data->getPluginSettingModule('pitchprint', 'publicKey'));
$event->getForm()->get('Pitchprint')->get('secretKey')->setData($data->getPluginSettingModule('pitchprint', 'secretKey'));
$event
->getForm()
->get('Pitchprint')
->get('publicKey')
->setData($data->getPluginSettingModule('pitchprint', 'publicKey'));
$event
->getForm()
->get('Pitchprint')
->get('secretKey')
->setData($data->getPluginSettingModule('pitchprint', 'secretKey'));
}
public function buildForm(FormBuilderInterface $builder, array $options)
@ -45,12 +51,12 @@ class ShopSettings implements Field
$builder->add('publicKey', TextType::class, array(
'label' => 'API Key',
'required' => false,
'mapped'=> false
'mapped' => false,
));
$builder->add('secretKey', TextType::class, array(
'label' => 'API Secret Key',
'required' => false,
'mapped'=> false
'mapped' => false,
));
return $builder;
@ -74,8 +80,15 @@ class ShopSettings implements Field
{
/** @var Shop $data */
$data = $event->getData();
$data->setPluginSettingModule('pitchprint', 'publicKey', $event->getForm()->get('Pitchprint')->get('publicKey')->getData());
$data->setPluginSettingModule('pitchprint', 'secretKey', $event->getForm()->get('Pitchprint')->get('secretKey')->getData());
$data->setPluginSettingModule(
'pitchprint',
'publicKey',
$event->getForm()->get('Pitchprint')->get('publicKey')->getData(),
);
$data->setPluginSettingModule(
'pitchprint',
'secretKey',
$event->getForm()->get('Pitchprint')->get('secretKey')->getData(),
);
}
}

View File

@ -1,16 +1,17 @@
<?php
namespace Plugin\System\PSC\Invoice\Controller\Backend;
use Symfony\Component\Security\Http\Attribute\IsGranted;
use Lexik\Bundle\JWTAuthenticationBundle\Services\JWTTokenManagerInterface;
use Symfony\Bridge\Twig\Attribute\Template;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Lexik\Bundle\JWTAuthenticationBundle\Services\JWTTokenManagerInterface;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted;
#[Route('/index')]
class IndexController extends AbstractController
{
#[Template('@PluginSystemPSCInvoiceControllerBackend/index/index.html.twig')]
#[Template('@PluginSystemPSCInvoice/backend/index/index.html.twig')]
#[IsGranted('ROLE_USER')]
#[Route(path: '/create', name: 'psc_backend_invoice_index_create')]
public function indexAction(JWTTokenManagerInterface $jwtManager)
@ -18,3 +19,4 @@ class IndexController extends AbstractController
return array('jwt' => $jwtManager->create($this->getUser()));
}
}

View File

@ -9,7 +9,7 @@ use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
class OrderController extends AbstractController
{
#[Template('@PluginSystemPSCInvoice/backend/order/index.html.twig')]
public function indexAction(null|\PSC\Shop\EntityBundle\Entity\Order $order, null|Order $orderDoc)
public function indexAction(?\PSC\Shop\EntityBundle\Entity\Order $order, ?Order $orderDoc)
{
return ['uuid' => $order ? $order->getUuid() : false];
}

View File

@ -922,10 +922,6 @@ html {
grid-column: span 2 / span 2;
}
.col-span-3{
grid-column: span 3 / span 3;
}
.col-span-4{
grid-column: span 4 / span 4;
}
@ -1863,6 +1859,11 @@ html {
background-color: rgb(239 68 68 / var(--tw-bg-opacity));
}
.bg-red-600{
--tw-bg-opacity: 1;
background-color: rgb(220 38 38 / var(--tw-bg-opacity));
}
.bg-slate-100{
--tw-bg-opacity: 1;
background-color: rgb(241 245 249 / var(--tw-bg-opacity));
@ -2781,6 +2782,11 @@ html {
background-color: rgb(220 38 38 / var(--tw-bg-opacity));
}
.hover\:bg-red-700:hover{
--tw-bg-opacity: 1;
background-color: rgb(185 28 28 / var(--tw-bg-opacity));
}
.hover\:bg-white:hover{
--tw-bg-opacity: 1;
background-color: rgb(255 255 255 / var(--tw-bg-opacity));
@ -2831,6 +2837,11 @@ html {
color: rgb(255 255 255 / var(--tw-text-opacity));
}
.hover\:text-yellow-700:hover{
--tw-text-opacity: 1;
color: rgb(161 98 7 / var(--tw-text-opacity));
}
.hover\:underline:hover{
text-decoration-line: underline;
}
@ -3190,6 +3201,11 @@ html {
color: rgb(255 255 255 / var(--tw-text-opacity));
}
:is(.dark .dark\:text-yellow-500){
--tw-text-opacity: 1;
color: rgb(234 179 8 / var(--tw-text-opacity));
}
:is(.dark .dark\:placeholder-gray-400)::-moz-placeholder{
--tw-placeholder-opacity: 1;
color: rgb(156 163 175 / var(--tw-placeholder-opacity));
@ -3247,6 +3263,11 @@ html {
background-color: rgb(88 25 19 / var(--tw-bg-opacity));
}
:is(.dark .dark\:hover\:text-blue-400:hover){
--tw-text-opacity: 1;
color: rgb(96 165 250 / var(--tw-text-opacity));
}
:is(.dark .dark\:hover\:text-blue-500:hover){
--tw-text-opacity: 1;
color: rgb(59 130 246 / var(--tw-text-opacity));
@ -3277,6 +3298,11 @@ html {
color: rgb(255 255 255 / var(--tw-text-opacity));
}
:is(.dark .dark\:hover\:text-yellow-400:hover){
--tw-text-opacity: 1;
color: rgb(250 204 21 / var(--tw-text-opacity));
}
:is(.dark .dark\:focus\:border-blue-500:focus){
--tw-border-opacity: 1;
border-color: rgb(59 130 246 / var(--tw-border-opacity));
@ -3407,6 +3433,10 @@ html {
width: 41.666667%;
}
.md\:w-9\/12{
width: 75%;
}
.md\:w-fit{
width: -moz-fit-content;
width: fit-content;
@ -3484,6 +3514,10 @@ 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;
}