... | ... |
@@ -14,6 +14,7 @@ use vonRotenberg\WeinanlieferungBundle\Model\WeinanlieferungLeseartModel; |
14 | 14 |
use vonRotenberg\WeinanlieferungBundle\Model\WeinanlieferungReservationModel; |
15 | 15 |
use vonRotenberg\WeinanlieferungBundle\Model\WeinanlieferungSlotsModel; |
16 | 16 |
use vonRotenberg\WeinanlieferungBundle\Model\WeinanlieferungStandortModel; |
17 |
+use vonRotenberg\WeinanlieferungBundle\Model\WeinanlieferungUnitModel; |
|
17 | 18 |
use Contao\ArrayUtil; |
18 | 19 |
|
19 | 20 |
ArrayUtil::arrayInsert($GLOBALS['BE_MOD'],1,[ |
... | ... |
@@ -21,7 +22,11 @@ ArrayUtil::arrayInsert($GLOBALS['BE_MOD'],1,[ |
21 | 22 |
'weinanlieferung' => [ |
22 | 23 |
'tables' => array('tl_vr_wa_standort', 'tl_vr_wa_slot', 'tl_vr_wa_rebsorte','tl_vr_wa_leseart','tl_vr_wa_lage','tl_vr_wa_reservation'), |
23 | 24 |
'stylesheet' => array('bundles/vonrotenbergweinanlieferung/css/backend.css') |
24 |
- ] |
|
25 |
+ ], |
|
26 |
+ 'wa_units' => [ |
|
27 |
+ 'tables' => array('tl_vr_wa_unit'), |
|
28 |
+ 'stylesheet' => array('bundles/vonrotenbergweinanlieferung/css/backend.css') |
|
29 |
+ ], |
|
25 | 30 |
] |
26 | 31 |
]); |
27 | 32 |
|
... | ... |
@@ -32,6 +37,7 @@ $GLOBALS['TL_MODELS']['tl_vr_wa_rebsorte'] = WeinanlieferungRebsorteModel::class |
32 | 37 |
$GLOBALS['TL_MODELS']['tl_vr_wa_leseart'] = WeinanlieferungLeseartModel::class; |
33 | 38 |
$GLOBALS['TL_MODELS']['tl_vr_wa_lage'] = WeinanlieferungLageModel::class; |
34 | 39 |
$GLOBALS['TL_MODELS']['tl_vr_wa_reservation'] = WeinanlieferungReservationModel::class; |
40 |
+$GLOBALS['TL_MODELS']['tl_vr_wa_unit'] = WeinanlieferungUnitModel::class; |
|
35 | 41 |
|
36 | 42 |
|
37 | 43 |
// Notification |
... | ... |
@@ -97,7 +97,7 @@ $GLOBALS['TL_DCA']['tl_vr_wa_reservation'] = array |
97 | 97 |
'palettes' => array |
98 | 98 |
( |
99 | 99 |
'__selector__' => array('checked_in'), |
100 |
- 'default' => 'pid,uid,behaelter,sorten,lage,ernteart,upload;{approval_legend},approved;{checkin_legend},checked_in' |
|
100 |
+ 'default' => 'pid,uid,behaelter,unit_id,unit_amount,sorten,lage,ernteart,upload;{approval_legend},approved;{checkin_legend},checked_in' |
|
101 | 101 |
), |
102 | 102 |
|
103 | 103 |
// Subpalettes |
... | ... |
@@ -154,6 +154,22 @@ $GLOBALS['TL_DCA']['tl_vr_wa_reservation'] = array |
154 | 154 |
'eval' => array('rgxp'=>'natural','tl_class'=>'w50'), |
155 | 155 |
'sql' => "int(4) unsigned NOT NULL default 0", |
156 | 156 |
), |
157 |
+ 'unit_id' => array |
|
158 |
+ ( |
|
159 |
+ 'exclude' => true, |
|
160 |
+ 'inputType' => 'select', |
|
161 |
+ 'foreignKey' => 'tl_vr_wa_unit.title', |
|
162 |
+ 'eval' => array('includeBlankOption'=>true,'chosen'=>true,'tl_class'=>'w50'), |
|
163 |
+ 'sql' => "int(10) unsigned NOT NULL default '0'", |
|
164 |
+ 'relation' => array('type' => 'belongsTo', 'load' => 'lazy') |
|
165 |
+ ), |
|
166 |
+ 'unit_amount' => array |
|
167 |
+ ( |
|
168 |
+ 'exclude' => true, |
|
169 |
+ 'inputType' => 'text', |
|
170 |
+ 'eval' => array('rgxp'=>'natural','tl_class'=>'w50'), |
|
171 |
+ 'sql' => "int(4) unsigned NOT NULL default 0", |
|
172 |
+ ), |
|
157 | 173 |
'sorten' => array |
158 | 174 |
( |
159 | 175 |
'exclude' => true, |
160 | 176 |
new file mode 100644 |
... | ... |
@@ -0,0 +1,99 @@ |
1 |
+<?php |
|
2 |
+ |
|
3 |
+/** |
|
4 |
+ * This file is part of contao-weinanlieferung-bundle. |
|
5 |
+ * |
|
6 |
+ * (c) vonRotenberg |
|
7 |
+ * |
|
8 |
+ * @license commercial |
|
9 |
+ */ |
|
10 |
+ |
|
11 |
+use Contao\DC_Table; |
|
12 |
+use Contao\DataContainer; |
|
13 |
+ |
|
14 |
+$GLOBALS['TL_DCA']['tl_vr_wa_unit'] = [ |
|
15 |
+ // Config |
|
16 |
+ 'config' => [ |
|
17 |
+ 'dataContainer' => DC_Table::class, |
|
18 |
+ 'sql' => [ |
|
19 |
+ 'keys' => [ |
|
20 |
+ 'id' => 'primary', |
|
21 |
+ ], |
|
22 |
+ ], |
|
23 |
+ ], |
|
24 |
+ |
|
25 |
+ // List |
|
26 |
+ 'list' => [ |
|
27 |
+ 'sorting' => [ |
|
28 |
+ 'mode' => DataContainer::MODE_SORTED, |
|
29 |
+ 'fields' => ['title'], |
|
30 |
+ 'panelLayout' => 'filter;sort,search,limit', |
|
31 |
+ ], |
|
32 |
+ 'label' => [ |
|
33 |
+ 'fields' => ['title', 'multiplier'], |
|
34 |
+ 'format' => '%s (x%s)' |
|
35 |
+ ], |
|
36 |
+ 'global_operations' => [ |
|
37 |
+ 'all' => [ |
|
38 |
+ 'href' => 'act=select', |
|
39 |
+ 'class' => 'header_edit_all', |
|
40 |
+ 'attributes' => 'onclick="Backend.getScrollOffset()" accesskey="e"', |
|
41 |
+ ], |
|
42 |
+ ], |
|
43 |
+ 'operations' => [ |
|
44 |
+ 'edit' => [ |
|
45 |
+ 'href' => 'act=edit', |
|
46 |
+ 'icon' => 'edit.gif', |
|
47 |
+ ], |
|
48 |
+ 'copy' => [ |
|
49 |
+ 'href' => 'act=paste&mode=copy', |
|
50 |
+ 'icon' => 'copy.svg', |
|
51 |
+ ], |
|
52 |
+ 'delete' => [ |
|
53 |
+ 'href' => 'act=delete', |
|
54 |
+ 'icon' => 'delete.gif', |
|
55 |
+ ], |
|
56 |
+ 'show' => [ |
|
57 |
+ 'icon' => 'show.gif', |
|
58 |
+ ], |
|
59 |
+ ], |
|
60 |
+ ], |
|
61 |
+ |
|
62 |
+ // Palettes |
|
63 |
+ 'palettes' => [ |
|
64 |
+ 'default' => 'title,multiplier', |
|
65 |
+ ], |
|
66 |
+ |
|
67 |
+ // Fields |
|
68 |
+ 'fields' => [ |
|
69 |
+ 'id' => [ |
|
70 |
+ 'sql' => "int(10) unsigned NOT NULL auto_increment", |
|
71 |
+ ], |
|
72 |
+ 'tstamp' => [ |
|
73 |
+ 'sql' => "int(10) unsigned NOT NULL default '0'", |
|
74 |
+ ], |
|
75 |
+ 'title' => [ |
|
76 |
+ 'exclude' => true, |
|
77 |
+ 'search' => true, |
|
78 |
+ 'inputType' => 'text', |
|
79 |
+ 'eval' => [ |
|
80 |
+ 'mandatory' => true, |
|
81 |
+ 'maxlength' => 255, |
|
82 |
+ 'tl_class' => 'w50', |
|
83 |
+ ], |
|
84 |
+ 'sql' => "varchar(255) NOT NULL default ''", |
|
85 |
+ ], |
|
86 |
+ 'multiplier' => [ |
|
87 |
+ 'exclude' => true, |
|
88 |
+ 'inputType' => 'text', |
|
89 |
+ 'eval' => [ |
|
90 |
+ 'mandatory' => true, |
|
91 |
+ 'rgxp' => 'natural', |
|
92 |
+ 'minval' => 1, |
|
93 |
+ 'maxlength' => 4, |
|
94 |
+ 'tl_class' => 'w50', |
|
95 |
+ ], |
|
96 |
+ 'sql' => "int(4) unsigned NOT NULL default 1", |
|
97 |
+ ], |
|
98 |
+ ], |
|
99 |
+]; |
... | ... |
@@ -4,6 +4,28 @@ |
4 | 4 |
<div class="frame__header"> |
5 | 5 |
<h3>Reservierung</h3> |
6 | 6 |
<div class="grid-md u-gap-2"> |
7 |
+ <div class="grid-c-6 mb-2 mb-0-md"> |
|
8 |
+ <div class="u-flex u-items-center u-gap-1"> |
|
9 |
+ <i class="icon-behaelter-outline"></i> |
|
10 |
+ <div class="t-label">Gebuchte Behälterkapazität</div> |
|
11 |
+ </div> |
|
12 |
+ <div class="">{{ buchung.behaelter }}</div> |
|
13 |
+ </div> |
|
14 |
+ {% if buchung is defined %} |
|
15 |
+ <div class="grid-c-6 mb-2 mb-0-md"> |
|
16 |
+ <div class="u-flex u-items-center u-gap-1"> |
|
17 |
+ <i class="icon-behaelter-outline"></i> |
|
18 |
+ <div class="t-label">Einheit</div> |
|
19 |
+ </div> |
|
20 |
+ {% set uTitle = buchung.unit_title|default('Behälter') %} |
|
21 |
+ {% set uAmount = buchung.unit_amount_display|default(0) %} |
|
22 |
+ {% if uAmount > 0 %} |
|
23 |
+ <div class="">{{ uAmount }} × {{ uTitle }}</div> |
|
24 |
+ {% else %} |
|
25 |
+ <div class="">{{ buchung.behaelter }} × {{ 'Behälter' }}</div> |
|
26 |
+ {% endif %} |
|
27 |
+ </div> |
|
28 |
+ {% endif %} |
|
7 | 29 |
<div class="grid-c-6 mb-2 mb-0-md"> |
8 | 30 |
<div class="u-flex u-items-center u-gap-1"> |
9 | 31 |
<i class="icon-uhr-outline"></i> |
... | ... |
@@ -42,22 +64,44 @@ |
42 | 64 |
<form hx-post="/_ajax/vr_wa/v1/slot?do=updateReservation" enctype="multipart/form-data"> |
43 | 65 |
<input type="hidden" name="id" value="{{ id }}"> |
44 | 66 |
<fieldset> |
45 |
- <label for="bok-behaelter"><strong>Liefernde Behältermenge</strong><sup class="text-danger">*</sup></label> |
|
46 |
- <select id="bok-behaelter" name="behaelter" required> |
|
47 |
- <option value="">-</option> |
|
48 |
- {% for option in buchen.behaelter %} |
|
49 |
- {% if option > slot.behaelterOcThreshold %} |
|
50 |
- <option value="{{ option }}"{{ buchung.behaelter == option ? ' selected' : '' }} data-oc>{{ option }} ({{ 'MSC.wa_approval_needed'|trans([], 'contao_default') }})</option> |
|
51 |
- {% else %} |
|
52 |
- <option value="{{ option }}"{{ buchung.behaelter == option ? ' selected' : '' }}>{{ option }}</option> |
|
53 |
- {% endif %} |
|
67 |
+ <label for="bok-unit"><strong>Einheit</strong><sup class="text-danger">*</sup></label> |
|
68 |
+ <select id="bok-unit" name="unit_id" required> |
|
69 |
+ {% for unit in buchen.units %} |
|
70 |
+ <option value="{{ unit.id }}" data-multiplier="{{ unit.multiplier }}"{% if buchung.unit_id == unit.id %} selected{% endif %}>{{ unit.title }}</option> |
|
54 | 71 |
{% endfor %} |
55 |
- {#{% for option in buchen.behaelter %} |
|
56 |
- <option value="{{ option }}"{{ buchung.behaelter == option ? ' selected' : '' }}>{{ option }}</option> |
|
57 |
- {% endfor %}#} |
|
58 | 72 |
</select> |
59 | 73 |
</fieldset> |
60 | 74 |
<fieldset> |
75 |
+ <label for="bok-unit-amount"><strong>Anzahl</strong><sup class="text-danger">*</sup></label> |
|
76 |
+ <select id="bok-unit-amount" name="unit_amount" required> |
|
77 |
+ </select> |
|
78 |
+ {#<div class="text-sm text-muted">Verbleibende Kapazität (Basis): {{ slot.behaelterAvailable }}</div>#} |
|
79 |
+ </fieldset> |
|
80 |
+ <script> |
|
81 |
+ (function(){ |
|
82 |
+ var container = document.getElementById('wa-booking-{{ id }}'); |
|
83 |
+ if(!container) return; |
|
84 |
+ var units = {{ buchen.units|json_encode|raw }}; |
|
85 |
+ var unitSel = container.querySelector('#bok-unit'); |
|
86 |
+ var amtSel = container.querySelector('#bok-unit-amount'); |
|
87 |
+ var selectedAmount = {{ buchung.unit_amount|default(0) }}; |
|
88 |
+ function fillAmounts(){ |
|
89 |
+ if(!unitSel||!amtSel) return; |
|
90 |
+ var id = parseInt(unitSel.value||0,10); |
|
91 |
+ var u = units.find(function(x){return x.id == id;}); |
|
92 |
+ if(!u){ amtSel.innerHTML=''; return; } |
|
93 |
+ var html = '<option value="">-</option>'; |
|
94 |
+ for(var i=1;i<=u.max_amount;i++){ html += '<option value="'+i+'"'+(selectedAmount==i?' selected':'')+'>'+i+'</option>'; } |
|
95 |
+ amtSel.innerHTML = html; |
|
96 |
+ } |
|
97 |
+ if(unitSel){ |
|
98 |
+ unitSel.onchange = null; |
|
99 |
+ unitSel.addEventListener('change', function(){ selectedAmount = 0; fillAmounts();}); |
|
100 |
+ } |
|
101 |
+ fillAmounts(); |
|
102 |
+ })(); |
|
103 |
+ </script> |
|
104 |
+ <fieldset> |
|
61 | 105 |
<legend>Anliefernde Rebsorte(n)<sup class="text-danger">*</sup></legend> |
62 | 106 |
{% for value,label in buchen.sorten %} |
63 | 107 |
<label><input type="checkbox" name="sorten[]" value="{{ value }}"{{ value in buchung.sorten|keys ? ' checked' : '' }}> <span class="checkable">{{ label }}</span></label><br> |
... | ... |
@@ -101,6 +145,30 @@ |
101 | 145 |
<script> |
102 | 146 |
(function ($) { |
103 | 147 |
|
148 |
+ // Define an idempotent initializer for this modal instance |
|
149 |
+ window.initWaBookingDetails{{ id }} = function() { |
|
150 |
+ var container = document.getElementById('wa-booking-{{ id }}'); |
|
151 |
+ if(!container) return; |
|
152 |
+ var units = {{ buchen.units|json_encode|raw }}; |
|
153 |
+ var unitSel = container.querySelector('#bok-unit'); |
|
154 |
+ var amtSel = container.querySelector('#bok-unit-amount'); |
|
155 |
+ var selectedAmount = {{ buchung.unit_amount|default(0) }}; |
|
156 |
+ function fillAmounts(){ |
|
157 |
+ if(!unitSel||!amtSel) return; |
|
158 |
+ var id = parseInt(unitSel.value||0,10); |
|
159 |
+ var u = units.find(function(x){return x.id == id;}); |
|
160 |
+ if(!u){ amtSel.innerHTML=''; return; } |
|
161 |
+ var html = '<option value="">-</option>'; |
|
162 |
+ for(var i=1;i<=u.max_amount;i++){ html += '<option value="'+i+'"'+(selectedAmount==i?' selected':'')+'>'+i+'</option>'; } |
|
163 |
+ amtSel.innerHTML = html; |
|
164 |
+ } |
|
165 |
+ if(unitSel){ |
|
166 |
+ unitSel.onchange = null; |
|
167 |
+ unitSel.addEventListener('change', function(){ selectedAmount = 0; fillAmounts();}); |
|
168 |
+ } |
|
169 |
+ fillAmounts(); |
|
170 |
+ }; |
|
171 |
+ |
|
104 | 172 |
window.modals = window.modals || [] |
105 | 173 |
window.modals.wa_bookings = window.modals.wa_bookings || [] |
106 | 174 |
|
... | ... |
@@ -116,11 +184,15 @@ |
116 | 184 |
overlay: true, |
117 | 185 |
closeOnClick: false, |
118 | 186 |
zIndex: 'auto', |
119 |
- addClass: '' |
|
187 |
+ addClass: '', |
|
188 |
+ onOpen: function(){ |
|
189 |
+ if (window.initWaBookingDetails{{ id }}) { window.initWaBookingDetails{{ id }}(); } |
|
190 |
+ } |
|
120 | 191 |
}).open(); |
121 | 192 |
} else { |
122 | 193 |
window.modals.wa_bookings.details{{ id }}.content.empty(); |
123 | 194 |
window.modals.wa_bookings.details{{ id }}.setContent($('#wa-booking-{{ id }}')).open(); |
195 |
+ if (window.initWaBookingDetails{{ id }}) { window.initWaBookingDetails{{ id }}(); } |
|
124 | 196 |
} |
125 | 197 |
|
126 | 198 |
})(jQuery); |
... | ... |
@@ -27,6 +27,22 @@ |
27 | 27 |
</div> |
28 | 28 |
<div class="">{{ buchung.behaelter }}</div> |
29 | 29 |
</div> |
30 |
+ <div class="grid-c-6 mb-2 mb-0-md"> |
|
31 |
+ <div class="u-flex u-items-center u-gap-1"> |
|
32 |
+ <i class="icon-behaelter-outline"></i> |
|
33 |
+ <div class="t-label">Erforderliche Eingaben</div> |
|
34 |
+ </div> |
|
35 |
+ <div class="">{{ checkin.expected }}</div> |
|
36 |
+ </div> |
|
37 |
+ <div class="grid-c-6 mb-2 mb-0-md"> |
|
38 |
+ <div class="u-flex u-items-center u-gap-1"> |
|
39 |
+ <i class="icon-behaelter-outline"></i> |
|
40 |
+ <div class="t-label">Einheit</div> |
|
41 |
+ </div> |
|
42 |
+ {% set uTitle = checkin.unit_title|default('Behälter') %} |
|
43 |
+ {% set uAmount = checkin.unit_amount_display|default(checkin.behaelter) %} |
|
44 |
+ <div class="">{{ uAmount }} × {{ uTitle }}</div> |
|
45 |
+ </div> |
|
30 | 46 |
|
31 | 47 |
<div class="grid-c-6 mb-2 mb-0-md"> |
32 | 48 |
<div class="u-flex u-items-center u-gap-1"> |
... | ... |
@@ -64,7 +80,8 @@ |
64 | 80 |
</datalist> |
65 | 81 |
|
66 | 82 |
<div class="grid-md u-gap-2"> |
67 |
- {% for i in 1..checkin.behaelter %} |
|
83 |
+ {% set expected = checkin.expected|default(checkin.behaelter) %} |
|
84 |
+ {% for i in 1..expected %} |
|
68 | 85 |
<div class="grid-c-12"> |
69 | 86 |
<div class="grid-md u-gap-2"> |
70 | 87 |
<div class="grid-c-6"> |
... | ... |
@@ -90,6 +90,9 @@ |
90 | 90 |
<div class="u-flex u-items-center u-gap-1"> |
91 | 91 |
<i class="icon-behaelter-outline"></i> |
92 | 92 |
{{ reservation.behaelter }} |
93 |
+ {% set uTitle = reservation.unit_title|default('Behälter') %} |
|
94 |
+ {% set uAmount = reservation.unit_amount_display|default(reservation.behaelter) %} |
|
95 |
+ <span class="text-sm text-muted"> ({{ uAmount }} × {{ uTitle }})</span> |
|
93 | 96 |
</div> |
94 | 97 |
</div> |
95 | 98 |
<div class="col-6"> |
... | ... |
@@ -138,22 +141,45 @@ |
138 | 141 |
<form hx-post="/_ajax/vr_wa/v1/slot?do=reservate" enctype="multipart/form-data"> |
139 | 142 |
<input type="hidden" name="id" value="{{ id }}"> |
140 | 143 |
<fieldset> |
141 |
- <label for="res-behaelter"><strong>Liefernde Behältermenge<sup class="text-danger">*</sup></strong></label> |
|
142 |
- <select id="res-behaelter" name="behaelter" required> |
|
143 |
- <option value="">-</option> |
|
144 |
- {% for option in buchen.behaelter %} |
|
145 |
- {% if option > slot.behaelterAvailable %} |
|
146 |
- <option value="{{ option }}" data-oc>{{ option }} ({{ 'MSC.wa_approval_needed'|trans([], 'contao_default') }})</option> |
|
147 |
- {% else %} |
|
148 |
- <option value="{{ option }}">{{ option }}</option> |
|
149 |
- {% endif %} |
|
144 |
+ <label for="res-unit"><strong>Einheit<sup class="text-danger">*</sup></strong></label> |
|
145 |
+ <select id="res-unit" name="unit_id" required> |
|
146 |
+ {% for unit in buchen.units %} |
|
147 |
+ <option value="{{ unit.id }}" data-multiplier="{{ unit.multiplier }}">{{ unit.title }}</option> |
|
150 | 148 |
{% endfor %} |
151 |
- {#{% for option in buchen.behaelter %} |
|
152 |
- <option value="{{ option }}">{{ option }}</option> |
|
153 |
- {% endfor %}#} |
|
154 | 149 |
</select> |
155 | 150 |
</fieldset> |
156 | 151 |
<fieldset> |
152 |
+ <label for="res-unit-amount"><strong>Anzahl<sup class="text-danger">*</sup></strong></label> |
|
153 |
+ <select id="res-unit-amount" name="unit_amount" required> |
|
154 |
+ <!-- options will be populated by script based on selected unit and capacity --> |
|
155 |
+ </select> |
|
156 |
+ {#<div class="text-sm text-muted">Verbleibende Kapazität (Basis): {{ slot.behaelterAvailable }}</div>#} |
|
157 |
+ </fieldset> |
|
158 |
+ <script> |
|
159 |
+ (function(){ |
|
160 |
+ var container = document.getElementById('wa-slot-{{ id }}'); |
|
161 |
+ if(!container) return; |
|
162 |
+ var units = {{ buchen.units|json_encode|raw }}; |
|
163 |
+ var unitSel = container.querySelector('#res-unit'); |
|
164 |
+ var amtSel = container.querySelector('#res-unit-amount'); |
|
165 |
+ function fillAmounts(){ |
|
166 |
+ if(!unitSel||!amtSel) return; |
|
167 |
+ var id = parseInt(unitSel.value||0,10); |
|
168 |
+ var u = units.find(function(x){return x.id == id;}); |
|
169 |
+ if(!u){ amtSel.innerHTML=''; return; } |
|
170 |
+ var html = '<option value="">-</option>'; |
|
171 |
+ for(var i=1;i<=u.max_amount;i++){ html += '<option value="'+i+'">'+i+'</option>'; } |
|
172 |
+ amtSel.innerHTML = html; |
|
173 |
+ } |
|
174 |
+ if(unitSel){ |
|
175 |
+ // avoid duplicate listeners by resetting |
|
176 |
+ unitSel.onchange = null; |
|
177 |
+ unitSel.addEventListener('change', fillAmounts, { once:false }); |
|
178 |
+ } |
|
179 |
+ fillAmounts(); |
|
180 |
+ })(); |
|
181 |
+ </script> |
|
182 |
+ <fieldset> |
|
157 | 183 |
<legend>Anliefernde Rebsorte(n)<sup class="text-danger">*</sup></legend> |
158 | 184 |
{% for value,label in buchen.sorten %} |
159 | 185 |
<label><input type="checkbox" name="sorten[]" value="{{ value }}"> <span class="checkable">{{ label }}</span></label><br> |
... | ... |
@@ -195,6 +221,29 @@ |
195 | 221 |
<script> |
196 | 222 |
(function ($) { |
197 | 223 |
|
224 |
+ // Define an idempotent initializer for this modal instance |
|
225 |
+ window.initWaSlotDetails{{ id }} = function() { |
|
226 |
+ var container = document.getElementById('wa-slot-{{ id }}'); |
|
227 |
+ if(!container) return; |
|
228 |
+ var units = {{ buchen.units|json_encode|raw }}; |
|
229 |
+ var unitSel = container.querySelector('#res-unit'); |
|
230 |
+ var amtSel = container.querySelector('#res-unit-amount'); |
|
231 |
+ function fillAmounts(){ |
|
232 |
+ if(!unitSel||!amtSel) return; |
|
233 |
+ var id = parseInt(unitSel.value||0,10); |
|
234 |
+ var u = units.find(function(x){return x.id == id;}); |
|
235 |
+ if(!u){ amtSel.innerHTML=''; return; } |
|
236 |
+ var html = '<option value="">-</option>'; |
|
237 |
+ for(var i=1;i<=u.max_amount;i++){ html += '<option value="'+i+'">'+i+'</option>'; } |
|
238 |
+ amtSel.innerHTML = html; |
|
239 |
+ } |
|
240 |
+ if(unitSel){ |
|
241 |
+ unitSel.onchange = null; |
|
242 |
+ unitSel.addEventListener('change', fillAmounts, { once:false }); |
|
243 |
+ } |
|
244 |
+ fillAmounts(); |
|
245 |
+ }; |
|
246 |
+ |
|
198 | 247 |
window.modals = window.modals || [] |
199 | 248 |
window.modals.wa_slots = window.modals.wa_slots || [] |
200 | 249 |
|
... | ... |
@@ -210,11 +259,15 @@ |
210 | 259 |
overlay: true, |
211 | 260 |
closeOnClick: false, |
212 | 261 |
zIndex: 'auto', |
213 |
- addClass: '' |
|
262 |
+ addClass: '', |
|
263 |
+ onOpen: function(){ |
|
264 |
+ if (window.initWaSlotDetails{{ id }}) { window.initWaSlotDetails{{ id }}(); } |
|
265 |
+ } |
|
214 | 266 |
}).open(); |
215 | 267 |
} else { |
216 | 268 |
window.modals.wa_slots.details{{ id }}.content.empty(); |
217 | 269 |
window.modals.wa_slots.details{{ id }}.setContent($('#wa-slot-{{ id }}')).open(); |
270 |
+ if (window.initWaSlotDetails{{ id }}) { window.initWaSlotDetails{{ id }}(); } |
|
218 | 271 |
} |
219 | 272 |
|
220 | 273 |
})(jQuery); |
... | ... |
@@ -134,10 +134,16 @@ |
134 | 134 |
<div class="t-label">Anliefernde Sorten</div> |
135 | 135 |
{{ booking.sorte|join(', ') }} |
136 | 136 |
</div> |
137 |
- <div class="col-3 behaelter icon-behaelter-outline"> |
|
138 |
- <div class="t-label">Gebuchte Behälterkapazität</div> |
|
137 |
+ <div class="col-1 behaelter icon-behaelter-outline"> |
|
138 |
+ <div class="t-label">Gebucht</div> |
|
139 | 139 |
{{ booking.behaelter }} |
140 | 140 |
</div> |
141 |
+ <div class="col-2 behaelter icon-behaelter-outline"> |
|
142 |
+ <div class="t-label">Einheit</div> |
|
143 |
+ {% set uTitle = booking.unit_title|default('Behälter') %} |
|
144 |
+ {% set uAmount = booking.unit_amount_display|default(booking.behaelter) %} |
|
145 |
+ {{ uAmount }} × {{ uTitle }} |
|
146 |
+ </div> |
|
141 | 147 |
<div class="additional-facts col-12 p-0 m-0"> |
142 | 148 |
<div class="row u-items-flex-start pb-0"> |
143 | 149 |
<div class="col-1"> |
... | ... |
@@ -69,6 +69,9 @@ |
69 | 69 |
<i class="icon-behaelter-outline"></i> |
70 | 70 |
<span class="t-label">Gebuchte Behälterkapazität</span> |
71 | 71 |
{{ booking.behaelter }} |
72 |
+ {% set uTitle = booking.unit_title|default('Behälter') %} |
|
73 |
+ {% set uAmount = booking.unit_amount_display|default(booking.behaelter) %} |
|
74 |
+ <span class="text-sm text-muted"> ({{ uAmount }} × {{ uTitle }})</span> |
|
72 | 75 |
</div> |
73 | 76 |
</div> |
74 | 77 |
<div class="grid-c-6 rebsorten bg-white p-1"> |
... | ... |
@@ -33,6 +33,7 @@ use vonRotenberg\WeinanlieferungBundle\Model\WeinanlieferungLeseartModel; |
33 | 33 |
use vonRotenberg\WeinanlieferungBundle\Model\WeinanlieferungRebsorteModel; |
34 | 34 |
use vonRotenberg\WeinanlieferungBundle\Model\WeinanlieferungReservationModel; |
35 | 35 |
use vonRotenberg\WeinanlieferungBundle\Model\WeinanlieferungSlotsModel; |
36 |
+use vonRotenberg\WeinanlieferungBundle\Model\WeinanlieferungUnitModel; |
|
36 | 37 |
|
37 | 38 |
/** |
38 | 39 |
* @Route("contao/weinanlieferung/buchungsliste", name=WeinanlieferungBookingsController::class, defaults={"_scope" = "backend"}) |
... | ... |
@@ -309,6 +310,22 @@ class WeinanlieferungBookingsController extends AbstractController |
309 | 310 |
} |
310 | 311 |
} |
311 | 312 |
|
313 |
+ // Compute unit display fields for backend list |
|
314 |
+ $unitTitle = 'Behälter'; |
|
315 |
+ $unitAmountDisplay = (int) $booking->behaelter; |
|
316 |
+ if (!empty($booking->unit_id) && (int)$booking->unit_id > 0) { |
|
317 |
+ $unitModel = WeinanlieferungUnitModel::findByPk((int)$booking->unit_id); |
|
318 |
+ if (null !== $unitModel) { |
|
319 |
+ $unitTitle = (string)$unitModel->title; |
|
320 |
+ } |
|
321 |
+ $unitAmountDisplay = (int) ($booking->unit_amount ?: 0); |
|
322 |
+ if ($unitAmountDisplay <= 0) { |
|
323 |
+ // Fallback just in case: derive amount by multiplier if available |
|
324 |
+ $mult = (int) ($unitModel ? $unitModel->multiplier : 0); |
|
325 |
+ $unitAmountDisplay = $mult > 0 ? max(1, (int) ($booking->behaelter / $mult)) : (int) $booking->behaelter; |
|
326 |
+ } |
|
327 |
+ } |
|
328 |
+ |
|
312 | 329 |
$arrData['days'][$day->dayBegin][$Slot->pid]['times'][$Slot->time]['items'][] = array_merge($booking->row(), [ |
313 | 330 |
'sorte' => $arrSorten, |
314 | 331 |
'ernteart' => $arrErnteart, |
... | ... |
@@ -317,7 +334,9 @@ class WeinanlieferungBookingsController extends AbstractController |
317 | 334 |
'standort' => $strStandort, |
318 | 335 |
'member' => $booking->getRelated('uid') !== null ? $booking->getRelated('uid')->row() : null, |
319 | 336 |
'behaelter_numbers' => $behaelterNumbers, |
320 |
- 'upload_file' => $uploadFile |
|
337 |
+ 'upload_file' => $uploadFile, |
|
338 |
+ 'unit_title' => $unitTitle, |
|
339 |
+ 'unit_amount_display' => $unitAmountDisplay, |
|
321 | 340 |
]); |
322 | 341 |
} |
323 | 342 |
} |
... | ... |
@@ -39,6 +39,7 @@ use vonRotenberg\WeinanlieferungBundle\Model\WeinanlieferungLeseartModel; |
39 | 39 |
use vonRotenberg\WeinanlieferungBundle\Model\WeinanlieferungRebsorteModel; |
40 | 40 |
use vonRotenberg\WeinanlieferungBundle\Model\WeinanlieferungReservationModel; |
41 | 41 |
use vonRotenberg\WeinanlieferungBundle\Model\WeinanlieferungSlotsModel; |
42 |
+use vonRotenberg\WeinanlieferungBundle\Model\WeinanlieferungUnitModel; |
|
42 | 43 |
|
43 | 44 |
/** |
44 | 45 |
* @Route("/_ajax/vr_wa/v1/slot", name="vr_wa_slot_ajax", defaults={"_scope" = "frontend", "_token_check" = false}) |
... | ... |
@@ -154,8 +155,25 @@ class SlotAjaxController extends AbstractController |
154 | 155 |
{ |
155 | 156 |
$arrSortenBooked = $Sorten->fetchEach('title'); |
156 | 157 |
}*/ |
158 |
+ // Compute unit display fields for this reservation |
|
159 |
+ $unitTitle = $GLOBALS['TL_LANG']['MSC']['wa_unit_base'] ?? 'Behälter'; |
|
160 |
+ $unitAmountDisplay = (int) $reservation->behaelter; |
|
161 |
+ if ((int)$reservation->unit_id > 0) { |
|
162 |
+ $unitModel = WeinanlieferungUnitModel::findByPk((int)$reservation->unit_id); |
|
163 |
+ if (null !== $unitModel) { |
|
164 |
+ $unitTitle = (string)$unitModel->title; |
|
165 |
+ } |
|
166 |
+ $unitAmountDisplay = (int) ($reservation->unit_amount ?: 0); |
|
167 |
+ if ($unitAmountDisplay <= 0) { |
|
168 |
+ $mult = (int) ($unitModel ? $unitModel->multiplier : 0); |
|
169 |
+ $unitAmountDisplay = $mult > 0 ? max(1, (int) ($reservation->behaelter / $mult)) : (int) $reservation->behaelter; |
|
170 |
+ } |
|
171 |
+ } |
|
172 |
+ |
|
157 | 173 |
$arrReservations[] = array_merge($reservation->row(),[ |
158 |
- 'sorten' => $arrSortenBooked |
|
174 |
+ 'sorten' => $arrSortenBooked, |
|
175 |
+ 'unit_title' => $unitTitle, |
|
176 |
+ 'unit_amount_display' => $unitAmountDisplay, |
|
159 | 177 |
]); |
160 | 178 |
} |
161 | 179 |
} |
... | ... |
@@ -207,6 +225,7 @@ class SlotAjaxController extends AbstractController |
207 | 225 |
'buchen' => [ |
208 | 226 |
'buchbar' => (boolean) ($Slot->behaelter*3 > $intReservedBehaelter), |
209 | 227 |
'behaelter' => range(min($intAvailableBehaelter,1),$Slot->behaelter*3-$intReservedBehaelter), |
228 |
+ 'units' => $this->getAvailableUnitsForCapacity($intAvailableBehaelter), |
|
210 | 229 |
'sorten' => $arrSorten, |
211 | 230 |
'lage' => $arrLage, |
212 | 231 |
'ernteart' => $arrErnteart, |
... | ... |
@@ -277,6 +296,7 @@ class SlotAjaxController extends AbstractController |
277 | 296 |
'buchen' => [ |
278 | 297 |
'buchbar' => (boolean) $intAvailableBehaelter, |
279 | 298 |
'behaelter' => range(min($intAvailableBehaelter,1),$intAvailableBehaelter), |
299 |
+ 'units' => $this->getAvailableUnitsForCapacity($intAvailableBehaelter), |
|
280 | 300 |
'sorten' => $arrSorten |
281 | 301 |
], |
282 | 302 |
]; |
... | ... |
@@ -396,7 +416,9 @@ class SlotAjaxController extends AbstractController |
396 | 416 |
} |
397 | 417 |
|
398 | 418 |
$intReservedBehaelter = $Slot->getReservedBehaelter(); |
399 |
- $intAvailableBehaelter = max($Booking->behaelter,$Slot->getAvailableBehaelter()); |
|
419 |
+ // While editing, available base units should include the current booking's base units, |
|
420 |
+ // because the edit will override the former amount. |
|
421 |
+ $intAvailableBehaelter = max(0, $Slot->getAvailableBehaelter() + (int) $Booking->behaelter); |
|
400 | 422 |
$intOcTreshold = $intAvailableBehaelter - $intReservedBehaelter + $Slot->behaelter; |
401 | 423 |
|
402 | 424 |
$arrData = array_merge($arrData,[ |
... | ... |
@@ -413,11 +435,30 @@ class SlotAjaxController extends AbstractController |
413 | 435 |
'sorten' => $arrSortenBooked, |
414 | 436 |
'ernteart' => $arrErnteartBooked, |
415 | 437 |
'lage' => $arrLagenBooked, |
438 |
+ // display fields |
|
439 |
+ 'unit_title' => (function() use ($Booking) { |
|
440 |
+ if ((int)$Booking->unit_id > 0) { |
|
441 |
+ $m = WeinanlieferungUnitModel::findByPk((int)$Booking->unit_id); |
|
442 |
+ return $m ? (string)$m->title : ($GLOBALS['TL_LANG']['MSC']['wa_unit_base'] ?? 'Behälter'); |
|
443 |
+ } |
|
444 |
+ return $GLOBALS['TL_LANG']['MSC']['wa_unit_base'] ?? 'Behälter'; |
|
445 |
+ })(), |
|
446 |
+ 'unit_amount_display' => (function() use ($Booking) { |
|
447 |
+ if ((int)$Booking->unit_id > 0) { |
|
448 |
+ $m = WeinanlieferungUnitModel::findByPk((int)$Booking->unit_id); |
|
449 |
+ $amount = (int) ($Booking->unit_amount ?: 0); |
|
450 |
+ if ($amount > 0) return $amount; |
|
451 |
+ $mult = (int) ($m ? $m->multiplier : 0); |
|
452 |
+ return $mult > 0 ? max(1, (int) ($Booking->behaelter / $mult)) : (int) $Booking->behaelter; |
|
453 |
+ } |
|
454 |
+ return (int) $Booking->behaelter; |
|
455 |
+ })(), |
|
416 | 456 |
]), |
417 | 457 |
'standort' => $Slot->getRelated('pid'), |
418 | 458 |
'buchen' => [ |
419 | 459 |
'buchbar' => (boolean) $intAvailableBehaelter, |
420 | 460 |
'behaelter' => range(min($intAvailableBehaelter,1),$Slot->behaelter*3-$Slot->getReservedBehaelter()+$Booking->behaelter), |
461 |
+ 'units' => $this->getAvailableUnitsForCapacity($intAvailableBehaelter), |
|
421 | 462 |
'sorten' => $arrSortenAvailable, |
422 | 463 |
'ernteart' => $arrErnteartAvailable, |
423 | 464 |
'lage' => $arrLagenAvailable, |
... | ... |
@@ -463,23 +504,41 @@ class SlotAjaxController extends AbstractController |
463 | 504 |
return new Response('Missing parameter',412); |
464 | 505 |
} |
465 | 506 |
|
466 |
- // Form validation |
|
507 |
+ // Form validation and unit capacity |
|
467 | 508 |
if (($Slot = WeinanlieferungSlotsModel::findByPk($_REQUEST['id'])) !== null) |
468 | 509 |
{ |
469 |
-// if (Input::post('behaelter') > $Slot->getAvailableBehaelter()) |
|
470 |
- if (Input::post('behaelter') > $Slot->behaelter*3-$Slot->getReservedBehaelter()) |
|
510 |
+ $unitId = (int) Input::post('unit_id'); |
|
511 |
+ $unitAmount = (int) Input::post('unit_amount'); |
|
512 |
+ $multiplier = 1; |
|
513 |
+ if ($unitId > 0) { |
|
514 |
+ if (($u = WeinanlieferungUnitModel::findByPk($unitId)) !== null) { |
|
515 |
+ $multiplier = max(1, (int) $u->multiplier); |
|
516 |
+ } |
|
517 |
+ } |
|
518 |
+ $baseUnitsRequested = $unitAmount > 0 ? $unitAmount * $multiplier : (int) Input::post('behaelter'); |
|
519 |
+ if ($baseUnitsRequested > $Slot->behaelter*3 - $Slot->getReservedBehaelter()) |
|
471 | 520 |
{ |
472 | 521 |
return $this->renderDetails(false,sprintf('<div class="toast toast--danger mx-0">Fehler: Es sind mittlerweile nur noch %s Behälter verfügbar.</div>',$Slot->getAvailableBehaelter())); |
473 | 522 |
} |
474 | 523 |
} |
475 | 524 |
$arrError = []; |
476 |
- foreach (['behaelter','sorten','ernteart','lage'] as $field) |
|
525 |
+ foreach (['sorten','ernteart','lage'] as $field) |
|
477 | 526 |
{ |
478 | 527 |
if (empty(Input::post($field))) |
479 | 528 |
{ |
480 | 529 |
$arrError = [$field]; |
481 | 530 |
} |
482 | 531 |
} |
532 |
+ // Require either (unit selection (allowing 0) with unit_amount, or behaelter fallback) |
|
533 |
+ $postedUnitId = Input::post('unit_id'); |
|
534 |
+ $hasUnitId = ($postedUnitId !== null && $postedUnitId !== '' && $postedUnitId !== false); |
|
535 |
+ if (!$hasUnitId || empty(Input::post('unit_amount'))) |
|
536 |
+ { |
|
537 |
+ if (empty(Input::post('behaelter'))) |
|
538 |
+ { |
|
539 |
+ $arrError[] = 'behaelter'; |
|
540 |
+ } |
|
541 |
+ } |
|
483 | 542 |
|
484 | 543 |
if (count($arrError)) |
485 | 544 |
{ |
... | ... |
@@ -523,11 +582,24 @@ class SlotAjaxController extends AbstractController |
523 | 582 |
$arrData['approved_on'] = $time; |
524 | 583 |
} |
525 | 584 |
|
585 |
+ // Determine fields to store |
|
586 |
+ $unitId = (int) Input::post('unit_id'); |
|
587 |
+ $unitAmount = (int) Input::post('unit_amount'); |
|
588 |
+ $multiplier = 1; |
|
589 |
+ if ($unitId > 0) { |
|
590 |
+ if (($u = WeinanlieferungUnitModel::findByPk($unitId)) !== null) { |
|
591 |
+ $multiplier = max(1, (int) $u->multiplier); |
|
592 |
+ } |
|
593 |
+ } |
|
594 |
+ $baseUnits = $unitAmount > 0 ? $unitAmount * $multiplier : (int) Input::post('behaelter'); |
|
595 |
+ |
|
526 | 596 |
$arrData = array_merge($arrData,[ |
527 | 597 |
'pid' => $_REQUEST['id'], |
528 | 598 |
'tstamp' => $time, |
529 | 599 |
'uid' => FrontendUser::getInstance()->id, |
530 |
- 'behaelter' => Input::post('behaelter'), |
|
600 |
+ 'behaelter' => $baseUnits, |
|
601 |
+ 'unit_id' => $unitId, |
|
602 |
+ 'unit_amount' => $unitAmount, |
|
531 | 603 |
'sorten' => $arrSorten, |
532 | 604 |
'ernteart' => $arrErnteart, |
533 | 605 |
'lage' => $arrLage |
... | ... |
@@ -588,20 +660,37 @@ class SlotAjaxController extends AbstractController |
588 | 660 |
/** @var WeinanlieferungSlotsModel $Slot */ |
589 | 661 |
if (($Slot = $Reservation->getRelated('pid')) !== null) |
590 | 662 |
{ |
591 |
-// if (Input::post('behaelter') > $Slot->getAvailableBehaelter()+$Reservation->behaelter) |
|
592 |
- if (Input::post('behaelter') > $Slot->behaelter*3-$Slot->getReservedBehaelter()+$Reservation->behaelter) |
|
663 |
+ $unitId = (int) Input::post('unit_id'); |
|
664 |
+ $unitAmount = (int) Input::post('unit_amount'); |
|
665 |
+ $multiplier = 1; |
|
666 |
+ if ($unitId > 0) { |
|
667 |
+ if (($u = WeinanlieferungUnitModel::findByPk($unitId)) !== null) { |
|
668 |
+ $multiplier = max(1, (int) $u->multiplier); |
|
669 |
+ } |
|
670 |
+ } |
|
671 |
+ $baseUnitsRequested = $unitAmount > 0 ? $unitAmount * $multiplier : (int) Input::post('behaelter'); |
|
672 |
+ if ($baseUnitsRequested > $Slot->behaelter*3 - $Slot->getReservedBehaelter() + $Reservation->behaelter) |
|
593 | 673 |
{ |
594 | 674 |
return $this->renderBooking(false,sprintf('<div class="toast toast--danger mx-0">Fehler: Es sind mittlerweile nur noch %s Behälter verfügbar.</div>',$Slot->getAvailableBehaelter()+$Reservation->behaelter)); |
595 | 675 |
} |
596 | 676 |
} |
597 | 677 |
$arrError = []; |
598 |
- foreach (['behaelter','sorten','ernteart','lage'] as $field) |
|
678 |
+ foreach (['sorten','ernteart','lage'] as $field) |
|
599 | 679 |
{ |
600 | 680 |
if (empty(Input::post($field))) |
601 | 681 |
{ |
602 | 682 |
$arrError = [$field]; |
603 | 683 |
} |
604 | 684 |
} |
685 |
+ $postedUnitId = Input::post('unit_id'); |
|
686 |
+ $hasUnitId = ($postedUnitId !== null && $postedUnitId !== '' && $postedUnitId !== false); |
|
687 |
+ if (!$hasUnitId || empty(Input::post('unit_amount'))) |
|
688 |
+ { |
|
689 |
+ if (empty(Input::post('behaelter'))) |
|
690 |
+ { |
|
691 |
+ $arrError[] = 'behaelter'; |
|
692 |
+ } |
|
693 |
+ } |
|
605 | 694 |
|
606 | 695 |
if (count($arrError)) |
607 | 696 |
{ |
... | ... |
@@ -644,7 +733,18 @@ class SlotAjaxController extends AbstractController |
644 | 733 |
} |
645 | 734 |
|
646 | 735 |
$Reservation->tstamp = $time; |
647 |
- $Reservation->behaelter = Input::post('behaelter'); |
|
736 |
+ // recompute base units |
|
737 |
+ $unitId = (int) Input::post('unit_id'); |
|
738 |
+ $unitAmount = (int) Input::post('unit_amount'); |
|
739 |
+ $multiplier = 1; |
|
740 |
+ if ($unitId > 0) { |
|
741 |
+ if (($u = WeinanlieferungUnitModel::findByPk($unitId)) !== null) { |
|
742 |
+ $multiplier = max(1, (int) $u->multiplier); |
|
743 |
+ } |
|
744 |
+ } |
|
745 |
+ $Reservation->behaelter = $unitAmount > 0 ? $unitAmount * $multiplier : (int) Input::post('behaelter'); |
|
746 |
+ $Reservation->unit_id = $unitId; |
|
747 |
+ $Reservation->unit_amount = $unitAmount; |
|
648 | 748 |
$Reservation->sorten = $arrSorten; |
649 | 749 |
$Reservation->ernteart = $arrErnteart; |
650 | 750 |
$Reservation->lage = $arrLage; |
... | ... |
@@ -768,6 +868,24 @@ class SlotAjaxController extends AbstractController |
768 | 868 |
'standort' => $Standort, |
769 | 869 |
'checkin' => [ |
770 | 870 |
'behaelter' => $Booking->behaelter, |
871 |
+ 'expected' => ($Booking->unit_amount ?? 0) > 0 ? (int)$Booking->unit_amount : (int)$Booking->behaelter, |
|
872 |
+ 'unit_title' => (function() use ($Booking) { |
|
873 |
+ if ((int)$Booking->unit_id > 0) { |
|
874 |
+ $m = WeinanlieferungUnitModel::findByPk((int)$Booking->unit_id); |
|
875 |
+ return $m ? (string)$m->title : ($GLOBALS['TL_LANG']['MSC']['wa_unit_base'] ?? 'Behälter'); |
|
876 |
+ } |
|
877 |
+ return $GLOBALS['TL_LANG']['MSC']['wa_unit_base'] ?? 'Behälter'; |
|
878 |
+ })(), |
|
879 |
+ 'unit_amount_display' => (function() use ($Booking) { |
|
880 |
+ if ((int)$Booking->unit_id > 0) { |
|
881 |
+ $m = WeinanlieferungUnitModel::findByPk((int)$Booking->unit_id); |
|
882 |
+ $amount = (int) ($Booking->unit_amount ?: 0); |
|
883 |
+ if ($amount > 0) return $amount; |
|
884 |
+ $mult = (int) ($m ? $m->multiplier : 0); |
|
885 |
+ return $mult > 0 ? max(1, (int) ($Booking->behaelter / $mult)) : (int) $Booking->behaelter; |
|
886 |
+ } |
|
887 |
+ return (int) $Booking->behaelter; |
|
888 |
+ })(), |
|
771 | 889 |
], |
772 | 890 |
'member' => $memberModel ? $memberModel->row() : null, |
773 | 891 |
'current_member' => $currentMemberModel ? $currentMemberModel->row() : null |
... | ... |
@@ -801,14 +919,15 @@ class SlotAjaxController extends AbstractController |
801 | 919 |
|
802 | 920 |
// Validate that we have the correct number of behaelter numbers |
803 | 921 |
$behaelterNumbers = Input::post('behaelter_numbers'); |
804 |
- if (!is_array($behaelterNumbers) || count($behaelterNumbers) != $Booking->behaelter) |
|
922 |
+ $expectedCount = ($Booking->unit_amount ?? 0) > 0 ? (int)$Booking->unit_amount : (int)$Booking->behaelter; |
|
923 |
+ if (!is_array($behaelterNumbers) || count($behaelterNumbers) != $expectedCount) |
|
805 | 924 |
{ |
806 | 925 |
// Prepare form data to preserve input values |
807 | 926 |
$formData = [ |
808 | 927 |
'behaelter_numbers' => $behaelterNumbers ?: [], |
809 | 928 |
'member_numbers' => Input::post('member_numbers') ?: [] |
810 | 929 |
]; |
811 |
- return $this->renderCheckin(false, '<div class="toast toast--danger mx-0">Bitte wählen Sie für jeden Behälter eine Nummer aus.</div>', $formData); |
|
930 |
+ return $this->renderCheckin(false, '<div class="toast toast--danger mx-0">Bitte wählen Sie für jede Einheit eine Nummer aus.</div>', $formData); |
|
812 | 931 |
} |
813 | 932 |
|
814 | 933 |
// Get member numbers from the form |
... | ... |
@@ -1117,4 +1236,39 @@ class SlotAjaxController extends AbstractController |
1117 | 1236 |
// If we got here, the number is valid |
1118 | 1237 |
return new Response(json_encode(['valid' => true]), 200, ['Content-Type' => 'application/json']); |
1119 | 1238 |
} |
1239 |
+ |
|
1240 |
+ protected function getAvailableUnitsForCapacity(int $availableBaseUnits): array |
|
1241 |
+ { |
|
1242 |
+ // Always include the base unit (Behälter) with multiplier 1 |
|
1243 |
+ $units = []; |
|
1244 |
+ $units[] = [ |
|
1245 |
+ 'id' => 0, |
|
1246 |
+ 'title' => $GLOBALS['TL_LANG']['MSC']['wa_unit_base'] ?? 'Behälter', |
|
1247 |
+ 'multiplier' => 1, |
|
1248 |
+ 'max_amount' => $availableBaseUnits, // one check-in per base unit |
|
1249 |
+ ]; |
|
1250 |
+ |
|
1251 |
+ // Load custom units from DB |
|
1252 |
+ if (($all = WeinanlieferungUnitModel::findAll()) !== null) { |
|
1253 |
+ foreach ($all as $unit) { |
|
1254 |
+ $mult = (int)($unit->multiplier ?? 0); |
|
1255 |
+ if ($mult < 1) { |
|
1256 |
+ continue; // skip invalid |
|
1257 |
+ } |
|
1258 |
+ // A unit fits if at least one of it can be placed into remaining capacity |
|
1259 |
+ $maxAmount = intdiv(max(0, $availableBaseUnits), $mult); |
|
1260 |
+ if ($maxAmount < 1) { |
|
1261 |
+ continue; // does not fit current capacity |
|
1262 |
+ } |
|
1263 |
+ $units[] = [ |
|
1264 |
+ 'id' => (int)$unit->id, |
|
1265 |
+ 'title' => (string)$unit->title, |
|
1266 |
+ 'multiplier' => $mult, |
|
1267 |
+ 'max_amount' => $maxAmount, |
|
1268 |
+ ]; |
|
1269 |
+ } |
|
1270 |
+ } |
|
1271 |
+ |
|
1272 |
+ return $units; |
|
1273 |
+ } |
|
1120 | 1274 |
} |
... | ... |
@@ -98,6 +98,21 @@ class WeinanlieferungBookedListModuleController extends AbstractFrontendModuleCo |
98 | 98 |
$arrLage = $Lage->fetchEach('title'); |
99 | 99 |
} |
100 | 100 |
|
101 |
+ // Compute unit display fields for frontend list |
|
102 |
+ $unitTitle = $GLOBALS['TL_LANG']['MSC']['wa_unit_base'] ?? 'Behälter'; |
|
103 |
+ $unitAmountDisplay = (int) $booking->behaelter; |
|
104 |
+ if (!empty($booking->unit_id) && (int)$booking->unit_id > 0) { |
|
105 |
+ $unitModel = \vonRotenberg\WeinanlieferungBundle\Model\WeinanlieferungUnitModel::findByPk((int)$booking->unit_id); |
|
106 |
+ if (null !== $unitModel) { |
|
107 |
+ $unitTitle = (string)$unitModel->title; |
|
108 |
+ } |
|
109 |
+ $unitAmountDisplay = (int) ($booking->unit_amount ?: 0); |
|
110 |
+ if ($unitAmountDisplay <= 0) { |
|
111 |
+ $mult = (int) ($unitModel ? $unitModel->multiplier : 0); |
|
112 |
+ $unitAmountDisplay = $mult > 0 ? max(1, (int) ($booking->behaelter / $mult)) : (int) $booking->behaelter; |
|
113 |
+ } |
|
114 |
+ } |
|
115 |
+ |
|
101 | 116 |
$arrData['days'][$day->dayBegin][] = array_merge($booking->row(), [ |
102 | 117 |
'sorte' => $arrSorten, |
103 | 118 |
'slot' => array_merge($Slot->row(),[ |
... | ... |
@@ -106,6 +121,8 @@ class WeinanlieferungBookedListModuleController extends AbstractFrontendModuleCo |
106 | 121 |
'standort' => $strStandort, |
107 | 122 |
'ernteart' => $arrErnteart, |
108 | 123 |
'lage' => $arrLage, |
124 |
+ 'unit_title' => $unitTitle, |
|
125 |
+ 'unit_amount_display' => $unitAmountDisplay, |
|
109 | 126 |
]); |
110 | 127 |
} |
111 | 128 |
} |
... | ... |
@@ -261,9 +261,10 @@ class WeinanlieferungReservationContainerListener |
261 | 261 |
|
262 | 262 |
// Only perform validation if the booking is not in the past |
263 | 263 |
if (!$isInPast) { |
264 |
- // Check if the number of container numbers matches the number of booked containers |
|
265 |
- if (count($behaelterNumbers) != $reservation->behaelter) { |
|
266 |
- throw new \Exception('Die Anzahl der Behälternummern muss mit der Anzahl der gebuchten Behälter übereinstimmen.'); |
|
264 |
+ // Check if the number of container numbers matches the number of required check-ins (unit amount or base behaelter) |
|
265 |
+ $expectedCount = ($reservation->unit_amount ?? 0) > 0 ? (int)$reservation->unit_amount : (int)$reservation->behaelter; |
|
266 |
+ if (count($behaelterNumbers) != $expectedCount) { |
|
267 |
+ throw new \Exception('Die Anzahl der Behälternummern muss mit der Anzahl der gebuchten Einheiten übereinstimmen.'); |
|
267 | 268 |
} |
268 | 269 |
|
269 | 270 |
// Filter out the special value 9999 ("Nummer nicht bekannt") for duplicate check |
270 | 271 |
new file mode 100644 |
... | ... |
@@ -0,0 +1,20 @@ |
1 |
+<?php |
|
2 |
+ |
|
3 |
+declare(strict_types=1); |
|
4 |
+ |
|
5 |
+/* |
|
6 |
+ * This file is part of contao-weinanlieferung-bundle. |
|
7 |
+ * |
|
8 |
+ * (c) vonRotenberg |
|
9 |
+ * |
|
10 |
+ * @license commercial |
|
11 |
+ */ |
|
12 |
+ |
|
13 |
+namespace vonRotenberg\WeinanlieferungBundle\Model; |
|
14 |
+ |
|
15 |
+use Contao\Model; |
|
16 |
+ |
|
17 |
+class WeinanlieferungUnitModel extends Model |
|
18 |
+{ |
|
19 |
+ protected static $strTable = 'tl_vr_wa_unit'; |
|
20 |
+} |