Backend Revocaton

This commit is contained in:
Thomas Peterson 2026-06-23 10:34:00 +02:00
parent 05ecf57822
commit 51061f1699
13 changed files with 342 additions and 113 deletions

View File

@ -123,6 +123,18 @@ class Orderpos
#[ORM\Column(name: 'external_approval_message', type: 'text', nullable: true)]
private ?string $externalApprovalMessage = null;
/**
* Wurde die Position vom Kunden widerrufen?
*/
#[ORM\Column(name: 'revoked', type: 'boolean', nullable: true)]
private ?bool $revoked = null;
/**
* Zeitpunkt des Widerrufs durch den Kunden.
*/
#[ORM\Column(name: 'revoked_at', type: 'datetime', nullable: true)]
private ?\DateTimeInterface $revokedAt = null;
/**
* @var Product
*/
@ -1059,4 +1071,30 @@ class Orderpos
{
$this->externalApprovalMessage = $message;
}
/**
* Wurde die Position vom Kunden widerrufen?
*/
public function isRevoked(): bool
{
return (bool) $this->revoked;
}
public function setRevoked(?bool $revoked): void
{
$this->revoked = $revoked;
}
/**
* Zeitpunkt des Widerrufs.
*/
public function getRevokedAt(): ?\DateTimeInterface
{
return $this->revokedAt;
}
public function setRevokedAt(?\DateTimeInterface $revokedAt): void
{
$this->revokedAt = $revokedAt;
}
}

View File

@ -584,6 +584,14 @@ class Product
#[ORM\Column(name: 'not_buy', type: 'boolean')]
protected $notBuy;
/**
* Widerrufsbutton im Bestellablauf aktivieren
*
* @var boolean
*/
#[ORM\Column(name: 'revoke_button_enable', type: 'boolean', nullable: true)]
protected $revokeButtonEnable;
/**
* Packageformat
*
@ -907,6 +915,7 @@ class Product
{
$this->enable = false;
$this->notBuy = false;
$this->revokeButtonEnable = false;
$this->private = false;
$this->displayNotInOverview = false;
$this->uuid = UUID::uuid4();
@ -1083,6 +1092,26 @@ class Product
$this->notBuy = $notBuy;
}
/**
* Ist der Widerrufsbutton für dieses Produkt aktiviert?
*
* @return boolean
*/
public function isRevokeButtonEnable()
{
return (bool) $this->revokeButtonEnable;
}
/**
* Setzt, ob der Widerrufsbutton für dieses Produkt aktiviert ist
*
* @param boolean $revokeButtonEnable
*/
public function setRevokeButtonEnable($revokeButtonEnable)
{
$this->revokeButtonEnable = $revokeButtonEnable;
}
/**
* Ist das Produkt privatisiert?
*

View File

@ -52,6 +52,21 @@ brutto: Brutto Preis
vat: MwSt.
positions: Positionen
and: und
nameDetails: Name / Details
propertiesEdition: Eigenschaften / Auflage
revokeStatus: Widerruf / Status
prices: Preise
actions: Aktionen
revokePossible: Widerruf möglich
revokeNotPossible: Widerruf nicht möglich
revokeDoneAt: Widerruf erfolgt am
revokeBy: durch
customerHint: Hinweis für den Kunden
printPartnerHint: Hinweis für den Druckpartner
notifyPrintPartner: Printpartner benachrichtigen
showCustomerInAccount: Dem Kunden in seinem Account anzeigen
label: Label
addresses: Adressen
billingaddress: Rechnungsanschrift
shippingaddress: Lieferanschrift

View File

@ -49,6 +49,21 @@ brutto: Gross Price
vat: Vat
positions: Positions
and: and
nameDetails: Name / Details
propertiesEdition: Properties / Quantity
revokeStatus: Revocation / Status
prices: Prices
actions: Actions
revokePossible: Revocation possible
revokeNotPossible: Revocation not possible
revokeDoneAt: Revoked on
revokeBy: by
customerHint: Note for the customer
printPartnerHint: Note for the print partner
notifyPrintPartner: Notify print partner
showCustomerInAccount: Show to customer in their account
label: Label
addresses: Addresses
billingaddress: Billing Address
shippingaddress: Shipping Address

View File

@ -433,79 +433,103 @@
<table class="min-w-full text-sm">
<thead class="bg-slate-100 border-t border-stroke">
<tr>
<th class="px-3 py-3 text-left font-medium text-gray-700">{{'Pos'|trans}}</th>
<th class="px-3 py-3 text-left font-medium text-gray-700">{{'name'|trans}}</th>
<th class="px-3 py-3 text-left font-medium text-gray-700">{{'wkpos1'|trans}}</th>
<th class="px-3 py-3 text-left font-medium text-gray-700">{{'wkpos2'|trans}}</th>
<th class="px-3 py-3 text-left font-medium text-gray-700">{{'properties'|trans}}</th>
<th class="px-3 py-3 text-left font-medium text-gray-700">{{'status'|trans}}</th>
<th class="px-3 py-3 text-right font-medium text-gray-700">{{'netto'|trans}}</th>
<th class="px-3 py-3 text-right font-medium text-gray-700">{{'vat'|trans}}</th>
<th class="px-3 py-3 text-right font-medium text-gray-700">{{'brutto'|trans}}</th>
<th class="px-3 py-3"></th>
<th class="px-3 py-3 text-left font-medium text-gray-700 w-10">{{'Pos'|trans}}</th>
<th class="px-3 py-3 text-left font-medium text-gray-700">{{'nameDetails'|trans}}</th>
<th class="px-3 py-3 text-left font-medium text-gray-700">{{'wkpos1'|trans}} {{'and'|trans}} {{'wkpos2'|trans}}</th>
<th class="px-3 py-3 text-left font-medium text-gray-700">{{'propertiesEdition'|trans}}</th>
<th class="px-3 py-3 text-left font-medium text-gray-700">{{'revokeStatus'|trans}}</th>
<th class="px-3 py-3 text-left font-medium text-gray-700">{{'prices'|trans}}</th>
<th class="px-3 py-3 text-left font-medium text-gray-700">{{'actions'|trans}}</th>
</tr>
</thead>
<tbody>
{% for pos in positions %}
{% set posModel = orderModel.getPositionByUuid(pos.obj.uuid) %}
<tr class="hover:bg-gray-50">
<td class="px-3 py-3 font-medium">{{ pos.obj.pos }}</td>
<td class="px-3 py-3">
<a href="{{ path('backend_production_product_edit', {uuid: pos.obj.product.uuid}) }}" class="text-psc-500 hover:underline font-medium">{{ pos.obj.product.title }}</a>
<div class="text-xs text-gray-500 mt-0.5">ArtNr intern: {{ pos.obj.product.nrIntern }}</div>
<div class="text-xs text-gray-500">{{ 'weight'|trans }}: {{ pos.obj.weight }}</div>
</td>
<td class="px-3 py-3 text-gray-700">{{ pos.obj.basketfield1 }}</td>
<td class="px-3 py-3 text-gray-700">{{ pos.obj.basketfield2 }}</td>
<td class="px-3 py-3"></td>
<td class="px-3 py-3">
<div class="relative inline-block text-left">
<button type="button" class="inline-flex justify-between items-center px-3 py-1.5 text-xs font-medium text-white bg-psc-500 hover:bg-psc-600 rounded-sm shadow-sm gap-1">
{{ orderStatuse.getPosStatusText(pos.obj.status) }}
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
</svg>
</button>
<div class="hidden absolute left-0 z-10 mt-1 w-48 rounded-sm shadow-xl bg-white border border-gray-200 py-1">
{% for status in orderStatuse.getOrderPosStatuse %}
<a class="flex items-center px-3 py-1.5 text-xs text-gray-700 hover:bg-psc-50 hover:text-psc-700 transition-colors" href="{{ path("psc_shop_order_backend_detail_switchposstatus", {order: order.uuid, pos: pos.obj.uuid, status: status.code}) }}">
{{ status.internalName|trans({}, 'status') }}
</a>
{% endfor %}
<tr class="align-top border-b-2 border-psc-500">
{# Pos #}
<td class="px-3 py-4 font-semibold text-gray-800">{{ pos.obj.pos }}</td>
{# Name / Details #}
<td class="px-3 py-4">
<a href="{{ path('backend_production_product_edit', {uuid: pos.obj.product.uuid}) }}" class="text-psc-500 hover:underline font-semibold">{{ pos.obj.product.title }}</a>
<div class="text-xs text-gray-500 mt-1">ArtNr intern: {{ pos.obj.product.nrIntern }}</div>
<div class="text-xs text-gray-500">{{ 'weight'|trans }}: {{ pos.obj.weight }} g</div>
{% if pos.objDoc.setConfig|length > 0 %}
<div class="mt-2">
<p class="text-xs font-medium text-gray-600 mb-0.5">Set bestehend aus:</p>
<ul class="text-xs text-gray-600 space-y-0.5 list-disc list-inside">
{% for item in pos.objDoc.setConfig %}
{% if (item.article_id|product) %}
<li><a href="{{ path('backend_production_product_edit', {uuid: item.article_id}) }}" target="_blank" class="text-psc-500 hover:underline">{{ (item.article_id|product).title }}</a> ({% if item.count == 0 %}{{ pos.count }}{% else %}{{ pos.count * item.count }}{% endif %})</li>
{% endif %}
{% endfor %}
</ul>
</div>
{% endif %}
{# Vorschaubilder über denselben Mechanismus wie in der Auftragsliste:
gated über das UploadTypeObject (canPreview) und die Position-Model-Uploads.
Andernfalls identisch zur Liste der "not available"-Platzhalter. #}
{% set uploadType = posModel.uploadTypeObject %}
<div class="mt-3">
{% if uploadType and uploadType.canPreview and posModel.uploads|length > 0 %}
<div class="flex flex-wrap gap-3">
{% for upload in posModel.uploads %}
<div class="flex flex-col items-center gap-1 w-24">
<img src="/apps/backend/order/upload/preview?path={{ upload.path|url_encode }}"
class="w-24 h-24 object-cover rounded-md border border-gray-200 shadow-sm"
title="{{ upload.fileName }}"
onerror="this.style.display='none'; this.nextElementSibling.style.display='flex';" />
<div style="display:none" class="w-24 h-24 rounded-md border border-gray-200 bg-gray-100 flex flex-col items-center justify-center text-gray-400 gap-1">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-7 h-7">
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" />
</svg>
<span class="text-xs">{{ upload.typ }}</span>
</div>
<span class="text-xs text-gray-500 w-24 truncate text-center" title="{{ upload.fileName }}">{{ upload.fileName }}</span>
</div>
{% endfor %}
</div>
{% else %}
<div class="shrink-0 w-28 h-28 rounded-md border-2 border-dashed border-gray-300 bg-white flex flex-col items-center justify-center text-gray-400">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-7 h-7 mb-1">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
<span class="text-xs">not available</span>
</div>
{% endif %}
</div>
<div class="mt-3 flex flex-wrap gap-2">
{% if shop.docLabelPosition != "" %}<a target="_blank" href="{{ path('psc_shop_order_backend_detail_print_pos', { uuid: order.uuid, posuuid: pos.obj.uuid, type: 3}) }}" class="inline-flex items-center gap-1 px-2.5 py-1.5 rounded-sm text-xs font-medium text-gray-700 bg-white border border-gray-300 hover:bg-gray-50 shadow-sm"><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"><path stroke-linecap="round" stroke-linejoin="round" d="M12 16.5V9.75m0 0l3 3m-3-3l-3 3M6.75 19.5a4.5 4.5 0 01-1.41-8.775 5.25 5.25 0 0110.233-2.33 3 3 0 013.758 3.848A3.752 3.752 0 0118 19.5H6.75z" /></svg>{{'label'|trans}}</a>{% endif %}
{% if shop.docJobticketPosition != "" %}<a target="_blank" href="{{ path('psc_shop_order_backend_detail_print_pos', { uuid: order.uuid, posuuid: pos.obj.uuid, type: 7}) }}" class="inline-flex items-center gap-1 px-2.5 py-1.5 rounded-sm text-xs font-medium text-gray-700 bg-white border border-gray-300 hover:bg-gray-50 shadow-sm"><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"><path stroke-linecap="round" stroke-linejoin="round" d="M12 16.5V9.75m0 0l3 3m-3-3l-3 3M6.75 19.5a4.5 4.5 0 01-1.41-8.775 5.25 5.25 0 0110.233-2.33 3 3 0 013.758 3.848A3.752 3.752 0 0118 19.5H6.75z" /></svg>{{'jobticket'|trans}}</a>{% endif %}
{% if pos.obj.layouterMode == 4 and pos.objDoc %}
<a target="_blank" href="/apps/steplayouter/index.html#/!?theme={{ shop.layouterTheme }}&article_uuid={{ pos.objDoc.layouterId }}&loadProduct=1&backendMode=1&shop_uuid={{ order.shop.uuid }}{% if order.shop.install.uid == 7 %}&theme=dp{% endif %}" class="inline-flex items-center gap-1 px-2.5 py-1.5 rounded-sm text-xs font-medium text-white bg-blue-500 hover:bg-blue-600 shadow-sm">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4"><path stroke-linecap="round" stroke-linejoin="round" d="M20.25 7.5l-.625 10.632a2.25 2.25 0 01-2.247 2.118H6.622a2.25 2.25 0 01-2.247-2.118L3.75 7.5M10 11.25h4M3.375 7.5h17.25c.621 0 1.125-.504 1.125-1.125v-1.5c0-.621-.504-1.125-1.125-1.125H3.375c-.621 0-1.125.504-1.125 1.125v1.5c0 .621.504 1.125 1.125 1.125z" /></svg>
Layouter öffnen
</a>
{% endif %}
</div>
</td>
<td class="px-3 py-3 text-right">{{ pos.obj.priceAllNetto|number_format(2, ',', '.') }}€</td>
<td class="px-3 py-3 text-right">{{ pos.obj.priceAllSteuer|number_format(2, ',', '.') }}€</td>
<td class="px-3 py-3 text-right font-medium">{{ pos.obj.priceAllBrutto|number_format(2, ',', '.') }}€</td>
<td class="px-3 py-3">
{% if pos.obj.layouterMode == 4 and pos.objDoc %}
<a target="_blank" href="/apps/steplayouter/index.html#/!?theme={{ shop.layouterTheme }}&article_uuid={{ pos.objDoc.layouterId }}&loadProduct=1&backendMode=1&shop_uuid={{ order.shop.uuid }}{% if order.shop.install.uid == 7 %}&theme=dp{% endif %}" class="inline-flex items-center gap-1 px-2.5 py-1.5 rounded-sm text-xs font-medium text-white bg-blue-500 hover:bg-blue-600 shadow-sm">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4"><path stroke-linecap="round" stroke-linejoin="round" d="M20.25 7.5l-.625 10.632a2.25 2.25 0 01-2.247 2.118H6.622a2.25 2.25 0 01-2.247-2.118L3.75 7.5M10 11.25h4M3.375 7.5h17.25c.621 0 1.125-.504 1.125-1.125v-1.5c0-.621-.504-1.125-1.125-1.125H3.375c-.621 0-1.125.504-1.125 1.125v1.5c0 .621.504 1.125 1.125 1.125z" /></svg>
Layouter öffnen
</a>
{# WK_Pos_1 / WK_Pos_2 Hinweise #}
<td class="px-3 py-4 max-w-xs">
{% if pos.obj.basketfield1 %}
<p class="text-xs font-semibold text-gray-700">{{'customerHint'|trans}}</p>
<p class="text-xs text-gray-600 whitespace-pre-line mb-3">{{ pos.obj.basketfield1 }}</p>
{% endif %}
{% if pos.obj.basketfield2 %}
<p class="text-xs font-semibold text-gray-700">{{'printPartnerHint'|trans}}</p>
<p class="text-xs text-gray-600 whitespace-pre-line">{{ pos.obj.basketfield2 }}</p>
{% endif %}
</td>
</tr>
<tr class="">
<td class="px-3 py-2"></td>
<td class="px-3 py-2">
{% if pos.objDoc.setConfig|length > 0 %}
<p class="text-xs font-medium text-gray-600 mb-1">Set bestehend aus:</p>
<ul class="text-xs text-gray-600 space-y-0.5 list-disc list-inside">
{% for item in pos.objDoc.setConfig %}
{% if (item.article_id|product) %}
<li><a href="{{ path('backend_production_product_edit', {uuid: item.article_id}) }}" target="_blank" class="text-psc-500 hover:underline">{{ (item.article_id|product).title }}</a> ({% if item.count == 0 %}{{ pos.count }}{% else %}{{ pos.count * item.count }}{% endif %})</li>
{% endif %}
{% endfor %}
</ul>
{% endif %}
</td>
<td class="px-3 py-2"></td>
<td class="px-3 py-2"></td>
<td class="px-3 py-2">
{# Eigenschaften / Auflage #}
<td class="px-3 py-4">
{% if pos.objCalc %}
<ul class="text-xs text-gray-600 space-y-0.5">
<ul class="text-xs text-gray-600 space-y-0.5 list-disc list-inside">
{% for opt in pos.objCalc.getOptions %}
{% if opt is not instanceof('\\PSC\\Library\\Calc\\Option\\Type\\Hidden') and opt.isValid() %}
<li>{{ opt.name }}: <span class="font-medium">{{ opt.value }}</span></li>
@ -522,20 +546,80 @@
{% endif %}
{% endfor %}
</td>
<td class="px-3 py-2" colspan="5">
<div class="flex flex-wrap gap-4">
<div class="flex gap-2 items-center">
{# Widerruf / Status #}
<td class="px-3 py-4">
{% if pos.obj.revoked %}
<div class="flex items-start gap-2 rounded-md border border-sky-200 bg-sky-50 px-3 py-2 text-xs text-sky-800 mb-3">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4 mt-0.5 shrink-0"><path stroke-linecap="round" stroke-linejoin="round" d="M11.25 11.25l.041-.02a.75.75 0 011.063.852l-.708 2.836a.75.75 0 001.063.853l.041-.021M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9-3.75h.008v.008H12V8.25z" /></svg>
<span>
{{'revokeDoneAt'|trans}}
{% if pos.obj.revokedAt %}<span class="font-medium">{{ pos.obj.revokedAt|date('d.m.Y H:i') }} Uhr</span>{% endif %}
{% if order.contact %}<br>{{'revokeBy'|trans}} {{ order.contact.firstname }} {{ order.contact.lastname }}{% endif %}
</span>
</div>
{% elseif pos.obj.product.revokeButtonEnable %}
<div class="flex items-center gap-2 rounded-md border border-emerald-200 bg-emerald-50 px-3 py-2 text-xs font-medium text-emerald-700 mb-3">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4 shrink-0"><path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>
{{'revokePossible'|trans}}
</div>
{% else %}
<div class="flex items-center gap-2 rounded-md border border-rose-200 bg-rose-50 px-3 py-2 text-xs font-medium text-rose-700 mb-3">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4 shrink-0"><path stroke-linecap="round" stroke-linejoin="round" d="M9.75 9.75l4.5 4.5m0-4.5l-4.5 4.5M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>
{{'revokeNotPossible'|trans}}
</div>
{% endif %}
<div class="relative inline-block text-left">
<button type="button" class="inline-flex justify-between items-center px-3 py-1.5 text-xs font-medium text-white bg-psc-500 hover:bg-psc-600 rounded-sm shadow-sm gap-1">
{{ orderStatuse.getPosStatusText(pos.obj.status) }}
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
</svg>
</button>
<div class="hidden absolute left-0 z-10 mt-1 w-48 rounded-sm shadow-xl bg-white border border-gray-200 py-1">
{% for status in orderStatuse.getOrderPosStatuse %}
<a class="flex items-center px-3 py-1.5 text-xs text-gray-700 hover:bg-psc-50 hover:text-psc-700 transition-colors" href="{{ path("psc_shop_order_backend_detail_switchposstatus", {order: order.uuid, pos: pos.obj.uuid, status: status.code}) }}">
{{ status.internalName|trans({}, 'status') }}
</a>
{% endfor %}
</div>
</div>
</td>
{# Preise #}
<td class="px-3 py-4">
<dl class="text-xs space-y-1 w-40">
<div class="flex justify-between gap-4">
<dt class="text-gray-500">{{'netto'|trans}}</dt>
<dd class="text-gray-800 text-right">{{ pos.obj.priceAllNetto|number_format(2, ',', '.') }} €</dd>
</div>
<div class="flex justify-between gap-4">
<dt class="text-gray-500">{{'vat'|trans}}</dt>
<dd class="text-gray-800 text-right">{{ pos.obj.priceAllSteuer|number_format(2, ',', '.') }} €</dd>
</div>
<div class="flex justify-between gap-4 border-t border-gray-100 pt-1">
<dt class="text-gray-700 font-semibold">{{'brutto'|trans}}</dt>
<dd class="text-gray-900 font-semibold text-right">{{ pos.obj.priceAllBrutto|number_format(2, ',', '.') }} €</dd>
</div>
</dl>
</td>
{# Aktionen #}
<td class="px-3 py-4">
<div class="flex flex-col gap-4 w-64">
<div>
{{ form_start(pos.formProd) }}
<div class="flex-1">{{ form_widget(pos.formProd.printPartnerEmail) }}</div>
<button class="w-full inline-flex items-center justify-center px-2.5 py-1.5 rounded-sm text-xs font-medium text-white bg-blue-500 hover:bg-blue-600 shadow-sm mb-1" type="submit">{{'notifyPrintPartner'|trans}}</button>
{{ form_widget(pos.formProd.printPartnerEmail) }}
{{ form_rest(pos.formProd) }}
<button class="inline-flex items-center px-2.5 py-1.5 rounded-sm text-xs font-medium text-white bg-blue-500 hover:bg-blue-600 shadow-sm shrink-0" type="submit">Printpartner benachrichtigen</button>
{{ form_end(pos.formProd) }}
</div>
<div class="flex gap-2 items-center">
<div>
{{ form_start(pos.formProdinfo) }}
<div class="flex-1">{{ form_widget(pos.formProdinfo.customerInfo) }}</div>
<button class="w-full inline-flex items-center justify-center px-2.5 py-1.5 rounded-sm text-xs font-medium text-white bg-blue-500 hover:bg-blue-600 shadow-sm mb-1" type="submit">{{'showCustomerInAccount'|trans}}</button>
{{ form_widget(pos.formProdinfo.customerInfo) }}
{{ form_rest(pos.formProdinfo) }}
<button class="inline-flex items-center px-2.5 py-1.5 rounded-sm text-xs font-medium text-white bg-blue-500 hover:bg-blue-600 shadow-sm shrink-0" type="submit">Dem Kunden in seinem Account anzeigen</button>
{{ form_end(pos.formProdinfo) }}
</div>
{% if posModel.reOrder %}
@ -545,45 +629,6 @@
</div>
</td>
</tr>
<tr class="">
<td class="px-3 py-2" colspan="10">
<div class="flex flex-wrap gap-2">
{% if shop.docOfferPosition != "" %}<a target="_blank" href="{{ path('psc_shop_order_backend_detail_print_pos', { uuid: order.uuid, posuuid: pos.obj.uuid, type: 5}) }}" class="inline-flex items-center gap-1 px-2.5 py-1 rounded-sm text-xs font-medium text-gray-700 bg-white border border-gray-300 hover:bg-gray-50 shadow-sm"><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"><path stroke-linecap="round" stroke-linejoin="round" d="M12 16.5V9.75m0 0l3 3m-3-3l-3 3M6.75 19.5a4.5 4.5 0 01-1.41-8.775 5.25 5.25 0 0110.233-2.33 3 3 0 013.758 3.848A3.752 3.752 0 0118 19.5H6.75z" /></svg>{{'offer'|trans}}</a>{% endif %}
{% if shop.docOrderPosition != "" %}<a target="_blank" href="{{ path('psc_shop_order_backend_detail_print_pos', { uuid: order.uuid, posuuid: pos.obj.uuid, type: 4}) }}" class="inline-flex items-center gap-1 px-2.5 py-1 rounded-sm text-xs font-medium text-gray-700 bg-white border border-gray-300 hover:bg-gray-50 shadow-sm"><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"><path stroke-linecap="round" stroke-linejoin="round" d="M12 16.5V9.75m0 0l3 3m-3-3l-3 3M6.75 19.5a4.5 4.5 0 01-1.41-8.775 5.25 5.25 0 0110.233-2.33 3 3 0 013.758 3.848A3.752 3.752 0 0118 19.5H6.75z" /></svg>{{'order'|trans}}</a>{% endif %}
{% if shop.docInvoicePosition != "" %}<a target="_blank" href="{{ path('psc_shop_order_backend_detail_print_pos', { uuid: order.uuid, posuuid: pos.obj.uuid, type: 1}) }}" class="inline-flex items-center gap-1 px-2.5 py-1 rounded-sm text-xs font-medium text-gray-700 bg-white border border-gray-300 hover:bg-gray-50 shadow-sm"><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"><path stroke-linecap="round" stroke-linejoin="round" d="M12 16.5V9.75m0 0l3 3m-3-3l-3 3M6.75 19.5a4.5 4.5 0 01-1.41-8.775 5.25 5.25 0 0110.233-2.33 3 3 0 013.758 3.848A3.752 3.752 0 0118 19.5H6.75z" /></svg>{{'invoice'|trans}}</a>{% endif %}
{% if shop.docDeliveryPosition != "" %}<a target="_blank" href="{{ path('psc_shop_order_backend_detail_print_pos', { uuid: order.uuid, posuuid: pos.obj.uuid, type: 2}) }}" class="inline-flex items-center gap-1 px-2.5 py-1 rounded-sm text-xs font-medium text-gray-700 bg-white border border-gray-300 hover:bg-gray-50 shadow-sm"><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"><path stroke-linecap="round" stroke-linejoin="round" d="M12 16.5V9.75m0 0l3 3m-3-3l-3 3M6.75 19.5a4.5 4.5 0 01-1.41-8.775 5.25 5.25 0 0110.233-2.33 3 3 0 013.758 3.848A3.752 3.752 0 0118 19.5H6.75z" /></svg>{{'deliverynote'|trans}}</a>{% endif %}
{% if shop.docLabelPosition != "" %}<a target="_blank" href="{{ path('psc_shop_order_backend_detail_print_pos', { uuid: order.uuid, posuuid: pos.obj.uuid, type: 3}) }}" class="inline-flex items-center gap-1 px-2.5 py-1 rounded-sm text-xs font-medium text-gray-700 bg-white border border-gray-300 hover:bg-gray-50 shadow-sm"><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"><path stroke-linecap="round" stroke-linejoin="round" d="M12 16.5V9.75m0 0l3 3m-3-3l-3 3M6.75 19.5a4.5 4.5 0 01-1.41-8.775 5.25 5.25 0 0110.233-2.33 3 3 0 013.758 3.848A3.752 3.752 0 0118 19.5H6.75z" /></svg>{{'labels'|trans}}</a>{% endif %}
{% if shop.docJobticketPosition != "" %}<a target="_blank" href="{{ path('psc_shop_order_backend_detail_print_pos', { uuid: order.uuid, posuuid: pos.obj.uuid, type: 7}) }}" class="inline-flex items-center gap-1 px-2.5 py-1 rounded-sm text-xs font-medium text-gray-700 bg-white border border-gray-300 hover:bg-gray-50 shadow-sm"><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"><path stroke-linecap="round" stroke-linejoin="round" d="M12 16.5V9.75m0 0l3 3m-3-3l-3 3M6.75 19.5a4.5 4.5 0 01-1.41-8.775 5.25 5.25 0 0110.233-2.33 3 3 0 013.758 3.848A3.752 3.752 0 0118 19.5H6.75z" /></svg>{{'jobticket'|trans}}</a>{% endif %}
{% if shop.docStornoPosition != "" %}<a target="_blank" href="{{ path('psc_shop_order_backend_detail_print_pos', { uuid: order.uuid, posuuid: pos.obj.uuid, type: 6}) }}" class="inline-flex items-center gap-1 px-2.5 py-1 rounded-sm text-xs font-medium text-gray-700 bg-white border border-gray-300 hover:bg-gray-50 shadow-sm"><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"><path stroke-linecap="round" stroke-linejoin="round" d="M12 16.5V9.75m0 0l3 3m-3-3l-3 3M6.75 19.5a4.5 4.5 0 01-1.41-8.775 5.25 5.25 0 0110.233-2.33 3 3 0 013.758 3.848A3.752 3.752 0 0118 19.5H6.75z" /></svg>{{'cancel'|trans}}</a>{% endif %}
</div>
</td>
</tr>
{% if pos.obj.uploads|length > 0 %}
<tr class="">
<td class="px-3 py-3" colspan="10">
<div class="flex flex-wrap gap-3">
{% for upload in pos.obj.uploads %}
<div class="flex flex-col items-center gap-1">
<img src="/apps/backend/order/upload/preview?path={{ upload.path|url_encode }}"
class="w-28 h-28 object-cover rounded-md border border-gray-200 shadow-sm"
title="{{ upload.name }}"
onerror="this.style.display='none'; this.nextElementSibling.style.display='flex';" />
<div style="display:none" class="w-28 h-28 rounded-md border border-gray-200 bg-gray-100 flex flex-col items-center justify-center text-gray-400 gap-1">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-7 h-7">
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" />
</svg>
<span class="text-xs">{{ upload.typ }}</span>
</div>
<span class="text-xs text-gray-500">{{ upload.name }}</span>
</div>
{% endfor %}
</div>
</td>
</tr>
{% endif %}
<tr>
<td colspan="10" class="h-1 bg-psc-500 p-0"></td>
</tr>
{% endfor %}
</tbody>
</table>

View File

@ -94,6 +94,7 @@ class ProductType extends AbstractType
->add('displayNoPrice', CheckboxType::class, ['required' => false, 'label' => 'notpriceshow'])
->add('displayNotInOverview', CheckboxType::class, ['required' => false, 'label' => 'notshow'])
->add('notBuy', CheckboxType::class, ['required' => false, 'label' => 'notshoworderbtn'])
->add('revokeButtonEnable', CheckboxType::class, ['required' => false, 'label' => 'enableRevokeButton'])
->add('notEdit', CheckboxType::class, ['required' => false, 'label' => 'Sellasproduct'])
->add('url', TextType::class, ['required' => true, 'label' => 'Url'])
->add('aribaUNSPSC', TextType::class, ['required' => false, 'label' => 'Ariba UNSPSC Identifikation'])

View File

@ -37,6 +37,7 @@ Extended: extended
Private: Privat
notshow: Nicht im Shop anzeigen
notshoworderbtn: „Bestellen“ Button nicht anzeigen
enableRevokeButton: Widerrufsbutton aktivieren
Sellasproduct: Als Produkt verkaufen
notpriceshow: Preis nicht anzeigen
pricenull: Bestellbar mit Preis 0€

View File

@ -36,6 +36,7 @@ Extended: Extended
Private: Private
notshow: Do not show in the shop
notshoworderbtn: Do not show the „Order" button
enableRevokeButton: Enable revocation button
Sellasproduct: Sell as a product
notpriceshow: Don't show price
pricenull: Can be ordered with a price of € 0

View File

@ -190,6 +190,9 @@
<div class="col-3">
{{ form_widget(form.asRequest) }}
</div>
<div class="col-3">
{{ form_widget(form.revokeButtonEnable) }}
</div>
</div>
<div class="row">
<div class="col-md-4">

View File

@ -209,6 +209,9 @@ a[href^="#formlayouter"] {display:none;}
<div class="col-3">
{{ form_widget(form.asRequest) }}
</div>
<div class="col-3">
{{ form_widget(form.revokeButtonEnable) }}
</div>
</div>
<div class="row">
<div class="col-md-4">

View File

@ -0,0 +1,12 @@
<?php
namespace PSC\System\UpdateBundle\Migrations;
class Version20260622120000 extends Base
{
public function migrateDatabase(): void
{
$connection = $this->entityManager->getConnection();
$connection->executeQuery("ALTER TABLE article ADD COLUMN revoke_button_enable TINYINT(1) NULL DEFAULT NULL;");
}
}

View File

@ -0,0 +1,13 @@
<?php
namespace PSC\System\UpdateBundle\Migrations;
class Version20260622140000 extends Base
{
public function migrateDatabase(): void
{
$connection = $this->entityManager->getConnection();
$connection->executeQuery("ALTER TABLE orderspos ADD COLUMN revoked TINYINT(1) NULL DEFAULT NULL;");
$connection->executeQuery("ALTER TABLE orderspos ADD COLUMN revoked_at DATETIME NULL DEFAULT NULL;");
}
}

View File

@ -982,6 +982,10 @@ html {
margin-bottom: 0px;
}
.mb-0\.5{
margin-bottom: 0.125rem;
}
.mb-1{
margin-bottom: 0.25rem;
}
@ -1206,6 +1210,10 @@ html {
height: 0.5rem;
}
.h-24{
height: 6rem;
}
.h-28{
height: 7rem;
}
@ -1326,6 +1334,10 @@ html {
width: 1rem;
}
.w-40{
width: 10rem;
}
.w-48{
width: 12rem;
}
@ -1587,10 +1599,6 @@ html {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.grid-cols-3{
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.grid-cols-4{
grid-template-columns: repeat(4, minmax(0, 1fr));
}
@ -1923,6 +1931,11 @@ html {
border-color: rgb(29 78 216 / var(--tw-border-opacity));
}
.border-emerald-200{
--tw-border-opacity: 1;
border-color: rgb(167 243 208 / var(--tw-border-opacity));
}
.border-gray-100{
--tw-border-opacity: 1;
border-color: rgb(243 244 246 / var(--tw-border-opacity));
@ -1963,6 +1976,16 @@ html {
border-color: rgb(239 68 68 / var(--tw-border-opacity));
}
.border-rose-200{
--tw-border-opacity: 1;
border-color: rgb(254 205 211 / var(--tw-border-opacity));
}
.border-sky-200{
--tw-border-opacity: 1;
border-color: rgb(186 230 253 / var(--tw-border-opacity));
}
.border-stone-200{
--tw-border-opacity: 1;
border-color: rgb(231 229 228 / var(--tw-border-opacity));
@ -2012,6 +2035,11 @@ html {
background-color: rgb(29 78 216 / var(--tw-bg-opacity));
}
.bg-emerald-50{
--tw-bg-opacity: 1;
background-color: rgb(236 253 245 / var(--tw-bg-opacity));
}
.bg-emerald-500{
--tw-bg-opacity: 1;
background-color: rgb(16 185 129 / var(--tw-bg-opacity));
@ -2124,11 +2152,21 @@ html {
background-color: rgb(220 38 38 / var(--tw-bg-opacity));
}
.bg-rose-50{
--tw-bg-opacity: 1;
background-color: rgb(255 241 242 / var(--tw-bg-opacity));
}
.bg-rose-500{
--tw-bg-opacity: 1;
background-color: rgb(244 63 94 / var(--tw-bg-opacity));
}
.bg-sky-50{
--tw-bg-opacity: 1;
background-color: rgb(240 249 255 / var(--tw-bg-opacity));
}
.bg-sky-500{
--tw-bg-opacity: 1;
background-color: rgb(14 165 233 / var(--tw-bg-opacity));
@ -2495,6 +2533,11 @@ html {
color: rgb(5 150 105 / var(--tw-text-opacity));
}
.text-emerald-700{
--tw-text-opacity: 1;
color: rgb(4 120 87 / var(--tw-text-opacity));
}
.text-gray-400{
--tw-text-opacity: 1;
color: rgb(156 163 175 / var(--tw-text-opacity));
@ -2595,6 +2638,16 @@ html {
color: rgb(127 29 29 / var(--tw-text-opacity));
}
.text-rose-700{
--tw-text-opacity: 1;
color: rgb(190 18 60 / var(--tw-text-opacity));
}
.text-sky-800{
--tw-text-opacity: 1;
color: rgb(7 89 133 / var(--tw-text-opacity));
}
.text-stone-500{
--tw-text-opacity: 1;
color: rgb(120 113 108 / var(--tw-text-opacity));