Browse code

Extend check-in to be able to assign each behaelter to a member number and export the check-in to a file per member number

Benjamin Roth authored on31/07/2025 14:27:06
Showing8 changed files
... ...
@@ -218,8 +218,25 @@ $GLOBALS['TL_DCA']['tl_vr_wa_reservation'] = array
218 218
         'behaelter_numbers' => array
219 219
         (
220 220
             'exclude'                 => true,
221
-            'inputType'               => 'text',
222
-            'eval'                    => array('tl_class'=>'clr', 'preserveTags'=>true),
221
+            'inputType'               => 'multiColumnWizard',
222
+            'eval'                    => array(
223
+                'tl_class' => 'clr',
224
+                'columnFields' => array
225
+                (
226
+                    'behaelter' => array
227
+                    (
228
+                        'label' => &$GLOBALS['TL_LANG']['MSC']['wa_behaelter_number'],
229
+                        'inputType' => 'text',
230
+                        'eval' => array('style' => 'width:200px', 'mandatory' => true, 'rgxp' => 'natural')
231
+                    ),
232
+                    'member' => array
233
+                    (
234
+                        'label' => &$GLOBALS['TL_LANG']['MSC']['wa_member_number'],
235
+                        'inputType' => 'text',
236
+                        'eval' => array('style' => 'width:200px', 'rgxp' => 'natural')
237
+                    )
238
+                )
239
+            ),
223 240
             'sql'                     => "text NULL"
224 241
         ),
225 242
     )
... ...
@@ -10,6 +10,8 @@
10 10
 
11 11
 $GLOBALS['TL_LANG']['MSC']['wa_sorte'] = 'Rebsorte';
12 12
 $GLOBALS['TL_LANG']['MSC']['wa_leseart'] = 'Leseart';
13
+$GLOBALS['TL_LANG']['MSC']['wa_behaelter_number'] = 'Behälternummer';
14
+$GLOBALS['TL_LANG']['MSC']['wa_member_number'] = 'Mitgliedsnummer';
13 15
 $GLOBALS['TL_LANG']['MSC']['wa_approval_needed'] = 'erfordert Freigabe';
14 16
 $GLOBALS['TL_LANG']['MSC']['wa_approval_pending'] = 'Freigabe ausstehend';
15 17
 
... ...
@@ -62,14 +62,25 @@
62 62
 
63 63
                 <div class="grid-md u-gap-2">
64 64
                     {% for i in 1..checkin.behaelter %}
65
-                        <div class="grid-c-6">
66
-                            <fieldset>
67
-                                <label for="behaelter-number-{{ i }}"><strong>Behälter {{ i }}</strong><sup class="text-danger">*</sup></label>
68
-                                <select id="behaelter-number-{{ i }}" name="behaelter_numbers[]" required data-loaded="false">
69
-                                    <option value="">-</option>
70
-                                    <!-- Options will be loaded via Ajax when the select is focused -->
71
-                                </select>
72
-                            </fieldset>
65
+                        <div class="grid-c-12">
66
+                            <div class="grid-md u-gap-2">
67
+                                <div class="grid-c-6">
68
+                                    <fieldset>
69
+                                        <label for="behaelter-number-{{ i }}"><strong>Behälter {{ i }}</strong><sup class="text-danger">*</sup></label>
70
+                                        <select id="behaelter-number-{{ i }}" name="behaelter_numbers[]" required data-loaded="false">
71
+                                            <option value="">-</option>
72
+                                            <!-- Options will be loaded via Ajax when the select is focused -->
73
+                                        </select>
74
+                                    </fieldset>
75
+                                </div>
76
+                                <div class="grid-c-6">
77
+                                    <fieldset>
78
+                                        <label for="member-number-{{ i }}"><strong>Mitgliedsnummer</strong></label>
79
+                                        <input type="text" id="member-number-{{ i }}" name="member_numbers[]" value="{{ current_member.memberno|default('') }}" placeholder="Mitgliedsnummer">
80
+                                        <p class="text-sm text-muted">Leer lassen für aktuelle Mitgliedsnummer: {{ current_member.memberno|default('Keine') }}</p>
81
+                                    </fieldset>
82
+                                </div>
83
+                            </div>
73 84
                         </div>
74 85
                     {% endfor %}
75 86
                 </div>
... ...
@@ -142,7 +142,9 @@
142 142
                                                     </div>
143 143
                                                     <div class="col-10 behaelter_nummern">
144 144
                                                         <div class="t-label">Eingecheckte Behälternummern</div>
145
-                                                        {{ booking.behaelter_numbers|join(', ') }}
145
+                                                        {% for behaelter in booking.behaelter_numbers %}
146
+                                                            {% if loop.index != 1 %}, {% endif %}{{ behaelter.behaelter }} [M:{{ behaelter.member }}]
147
+                                                        {% endfor %}
146 148
                                                     </div>
147 149
                                                 {% endif %}
148 150
                                                 <div class="col px-1 u-text-right action">
... ...
@@ -19,6 +19,7 @@ use Contao\Date;
19 19
 use Contao\Environment;
20 20
 use Contao\FrontendUser;
21 21
 use Contao\Input;
22
+use Contao\MemberModel;
22 23
 use Contao\StringUtil;
23 24
 use Contao\System;
24 25
 use Doctrine\DBAL\Connection;
... ...
@@ -273,6 +274,23 @@ class WeinanlieferungBookingsController extends AbstractController
273 274
                         $strStandort = $Standort->title;
274 275
                     }
275 276
 
277
+                    $behaelterNumbers = json_decode($booking->behaelter_numbers, true);
278
+                    $isNewFormat = isset($behaelterNumbers[0]) && is_array($behaelterNumbers[0]) && isset($behaelterNumbers[0]['behaelter']);
279
+
280
+                    // If it's the old format, convert it to the new format for compatibility
281
+                    if (!$isNewFormat) {
282
+                        $oldFormat = $behaelterNumbers;
283
+                        $memberModel = MemberModel::findById($booking->uid);
284
+
285
+                        $behaelterNumbers = [];
286
+                        foreach ($oldFormat as $number) {
287
+                            $behaelterNumbers[] = [
288
+                                'behaelter' => $number,
289
+                                'member' => $memberModel->memberno
290
+                            ];
291
+                        }
292
+                    }
293
+
276 294
                     $arrData['days'][$day->dayBegin][$Slot->pid]['times'][$Slot->time]['items'][] = array_merge($booking->row(), [
277 295
                         'sorte'              => $arrSorten,
278 296
                         'ernteart' => $arrErnteart,
... ...
@@ -280,7 +298,7 @@ class WeinanlieferungBookingsController extends AbstractController
280 298
                         'slot'  => $Slot->row(),
281 299
                         'standort' => $strStandort,
282 300
                         'member' => $booking->getRelated('uid') !== null ? $booking->getRelated('uid')->row() : null,
283
-                        'behaelter_numbers' => \json_decode($booking->behaelter_numbers)
301
+                        'behaelter_numbers' => $behaelterNumbers
284 302
                     ]);
285 303
                 }
286 304
             }
... ...
@@ -24,6 +24,7 @@ use Contao\FormFileUpload;
24 24
 use Contao\Frontend;
25 25
 use Contao\FrontendUser;
26 26
 use Contao\Input;
27
+use Contao\MemberModel;
27 28
 use Contao\StringUtil;
28 29
 use Contao\System;
29 30
 use Doctrine\DBAL\Connection;
... ...
@@ -746,6 +747,10 @@ class SlotAjaxController extends AbstractController
746 747
             }
747 748
         }
748 749
 
750
+        // Load the member model for the booking's user
751
+        $memberModel = MemberModel::findById($Booking->uid);
752
+        $currentMemberModel = MemberModel::findById(FrontendUser::getInstance()->id);
753
+
749 754
         $arrData = array_merge($arrData, [
750 755
             'modal' => $blnModal,
751 756
             'id' => $Booking->id,
... ...
@@ -760,7 +765,9 @@ class SlotAjaxController extends AbstractController
760 765
             'standort' => $Standort,
761 766
             'checkin' => [
762 767
                 'behaelter' => $Booking->behaelter,
763
-            ]
768
+            ],
769
+            'member' => $memberModel ? $memberModel->row() : null,
770
+            'current_member' => $currentMemberModel ? $currentMemberModel->row() : null
764 771
         ]);
765 772
 
766 773
         if (!empty($error))
... ...
@@ -791,6 +798,19 @@ class SlotAjaxController extends AbstractController
791 798
             return $this->renderCheckin(false, '<div class="toast toast--danger mx-0">Bitte wählen Sie für jeden Behälter eine Nummer aus.</div>');
792 799
         }
793 800
 
801
+        // Get member numbers from the form
802
+        $memberNumbers = Input::post('member_numbers');
803
+
804
+        // If member numbers are not provided or not an array, initialize an empty array
805
+        if (!is_array($memberNumbers) || count($memberNumbers) != $Booking->behaelter)
806
+        {
807
+            $memberNumbers = array_fill(0, count($behaelterNumbers), '');
808
+        }
809
+
810
+        // Get the current member's number to use as fallback
811
+        $currentMember = MemberModel::findById(FrontendUser::getInstance()->id);
812
+        $currentMemberNo = $currentMember ? $currentMember->memberno : '';
813
+
794 814
         // Filter out the special value 9999 ("Nummer nicht bekannt") for duplicate check
795 815
         $numbersForDuplicateCheck = array_filter($behaelterNumbers, function($number) {
796 816
             return $number !== '9999';
... ...
@@ -802,11 +822,21 @@ class SlotAjaxController extends AbstractController
802 822
             return $this->renderCheckin(false, '<div class="toast toast--danger mx-0">Jede Nummer kann nur einmal verwendet werden.</div>');
803 823
         }
804 824
 
825
+        // Create combined array with behaelter numbers and member numbers
826
+        $combinedData = [];
827
+        foreach ($behaelterNumbers as $index => $behaelterNumber) {
828
+            $memberNumber = !empty($memberNumbers[$index]) ? $memberNumbers[$index] : $currentMemberNo;
829
+            $combinedData[] = [
830
+                'behaelter' => $behaelterNumber,
831
+                'member' => $memberNumber
832
+            ];
833
+        }
834
+
805 835
         // Save the check-in data
806 836
         $time = time();
807 837
         $Booking->checked_in = '1';
808 838
         $Booking->checked_in_on = $time;
809
-        $Booking->behaelter_numbers = json_encode($behaelterNumbers);
839
+        $Booking->behaelter_numbers = json_encode($combinedData);
810 840
         $Booking->tstamp = $time;
811 841
         $Booking->save();
812 842
 
... ...
@@ -115,6 +115,22 @@ class CheckInCompletedListener implements EventSubscriberInterface
115 115
             return;
116 116
         }
117 117
 
118
+        // Check if we have the new format (array of objects with behaelter and member)
119
+        // or the old format (simple array of behaelter numbers)
120
+        $isNewFormat = isset($behaelterNumbers[0]) && is_array($behaelterNumbers[0]) && isset($behaelterNumbers[0]['behaelter']);
121
+
122
+        // If it's the old format, convert it to the new format for compatibility
123
+        if (!$isNewFormat) {
124
+            $oldFormat = $behaelterNumbers;
125
+            $behaelterNumbers = [];
126
+            foreach ($oldFormat as $number) {
127
+                $behaelterNumbers[] = [
128
+                    'behaelter' => $number,
129
+                    'member' => $memberModel->memberno
130
+                ];
131
+            }
132
+        }
133
+
118 134
         // Format check-in date
119 135
         $checkInDate = date('d.m.Y', $reservationData['checked_in_on']);
120 136
 
... ...
@@ -166,16 +182,41 @@ class CheckInCompletedListener implements EventSubscriberInterface
166 182
             'vollernter' => 'V'
167 183
         ];
168 184
 
169
-        // Prepare CSV data
170
-        $csvData = [];
185
+        // Group CSV data by member number
186
+        $csvDataByMember = [];
187
+        $memberNames = []; // Store member names for logging
188
+
189
+        // For each behaelter, create a line in the CSV and group by member number
190
+        foreach ($behaelterNumbers as $behaelterData) {
191
+            // Get behaelter number and member number from the data
192
+            $behaelterNumber = $isNewFormat ? $behaelterData['behaelter'] : $behaelterData;
193
+            $memberNo = $isNewFormat && isset($behaelterData['member']) ? $behaelterData['member'] : $memberModel->memberno;
171 194
 
172
-        // For each behaelter, create a line in the CSV
173
-        foreach ($behaelterNumbers as $behaelterNumber) {
174 195
             // Skip special value 9999 if needed
175 196
             if ($behaelterNumber === '9999') {
176 197
                 continue;
177 198
             }
178 199
 
200
+            // If member number is empty, use the booking member's number
201
+            if (empty($memberNo)) {
202
+                $memberNo = $memberModel->memberno;
203
+            }
204
+
205
+            // Get member name - use the booking member's name as default
206
+            $memberName = $memberModel->firstname . ' ' . $memberModel->lastname;
207
+
208
+            // If the member number is different from the booking member's number,
209
+            // try to find the corresponding member to get their name
210
+            if ($memberNo !== $memberModel->memberno) {
211
+                $otherMember = MemberModel::findByMemberno($memberNo);
212
+                if ($otherMember) {
213
+                    $memberName = $otherMember->firstname . ' ' . $otherMember->lastname;
214
+                }
215
+            }
216
+
217
+            // Store member name for logging
218
+            $memberNames[$memberNo] = $memberName;
219
+
179 220
             // Use the first sorte, leseart, and lage if available
180 221
             $rebsorteId = !empty($sortenData) ? $sortenData[0]['rebsorte_id'] : '';
181 222
             $rebsorteTitle = !empty($sortenData) ? $sortenData[0]['rebsorte_title'] : '';
... ...
@@ -193,10 +234,14 @@ class CheckInCompletedListener implements EventSubscriberInterface
193 234
                 }
194 235
             }
195 236
 
196
-            // Create CSV line
197
-            $csvData[] = [
198
-                $memberModel->memberno, // member id
199
-                $memberModel->firstname . ' ' . $memberModel->lastname, // member first and lastname
237
+            // Create CSV line and add to the appropriate member group
238
+            if (!isset($csvDataByMember[$memberNo])) {
239
+                $csvDataByMember[$memberNo] = [];
240
+            }
241
+
242
+            $csvDataByMember[$memberNo][] = [
243
+                $memberNo, // member id (now using the member number associated with this behaelter)
244
+                $memberName, // member first and lastname
200 245
                 $rebsorteId, // rebsorte identifikator
201 246
                 $rebsorteTitle, // rebsorte title
202 247
                 $lageId, // lage identifikator
... ...
@@ -206,25 +251,47 @@ class CheckInCompletedListener implements EventSubscriberInterface
206 251
                 $ernteart, // ernteart (H for handlese, V for vollernter)
207 252
                 $behaelterNumber, // behaelter number
208 253
                 $checkInDate, // check-in date
209
-                $reservationData['behaelter'] // behaelter amount for the whole checked-in booking
254
+//                $reservationData['behaelter'] // behaelter amount for the whole checked-in booking
210 255
             ];
211 256
         }
212 257
 
213
-        // Generate CSV file
214
-        $filename = $reservationData['id'] . '_' . $memberModel->memberno . '.csv';
215
-        $filepath = $exportDir . '/' . $filename;
258
+        // Generate a separate CSV file for each member
259
+        $generatedFiles = [];
260
+        foreach ($csvDataByMember as $memberNo => $csvData) {
261
+            // Generate CSV file with the naming scheme "bookingId_memberno.csv"
262
+            $filename = $reservationData['id'] . '_' . $memberNo . '.csv';
263
+            $filepath = $exportDir . '/' . $filename;
264
+            $generatedFiles[] = $filepath;
216 265
 
217
-        $file = fopen($filepath, 'w');
266
+            $file = fopen($filepath, 'w');
218 267
 
219
-        foreach ($csvData as $line) {
220
-            fputcsv($file, $line, ';');
221
-        }
268
+            foreach ($csvData as $line) {
269
+                $line[] = count($csvData);
270
+                fputcsv($file, $line, ';');
271
+            }
222 272
 
223
-        fclose($file);
273
+            fclose($file);
274
+
275
+            $this->logger->log(
276
+                LogLevel::INFO,
277
+                sprintf('CSV export created for reservation ID: %s, member: %s (%s) at %s',
278
+                    $reservationData['id'],
279
+                    $memberNo,
280
+                    $memberNames[$memberNo],
281
+                    $filepath
282
+                ),
283
+                ['contao' => new ContaoContext(__METHOD__, 'CHECK_IN_CSV_EXPORT')]
284
+            );
285
+        }
224 286
 
287
+        // Log summary of all generated files
225 288
         $this->logger->log(
226 289
             LogLevel::INFO,
227
-            sprintf('CSV export created for reservation ID: %s at %s', $reservationData['id'], $filepath),
290
+            sprintf('CSV export completed for reservation ID: %s. Generated %d file(s) for %d member(s).',
291
+                $reservationData['id'],
292
+                count($generatedFiles),
293
+                count($csvDataByMember)
294
+            ),
228 295
             ['contao' => new ContaoContext(__METHOD__, 'CHECK_IN_CSV_EXPORT')]
229 296
         );
230 297
     }
... ...
@@ -175,7 +175,34 @@ class WeinanlieferungReservationContainerListener
175 175
     {
176 176
         if (!empty($varValue))
177 177
         {
178
-            $varValue = implode(', ',\json_decode($varValue));
178
+            $decodedValue = \json_decode($varValue, true);
179
+
180
+            // Check if we have the new format (array of objects with behaelter and member)
181
+            // or the old format (simple array of behaelter numbers)
182
+            $isNewFormat = isset($decodedValue[0]) && is_array($decodedValue[0]) && isset($decodedValue[0]['behaelter']);
183
+
184
+            if ($isNewFormat) {
185
+                // The data is already in the correct format for the multiColumnWizard
186
+                return $decodedValue;
187
+            } else {
188
+                // Convert old format to new format
189
+                $result = [];
190
+
191
+                // Get the member associated with this reservation as fallback
192
+                $reservation = \vonRotenberg\WeinanlieferungBundle\Model\WeinanlieferungReservationModel::findByPk($dc->id);
193
+                $memberId = $reservation ? $reservation->uid : 0;
194
+                $memberModel = \Contao\MemberModel::findById($memberId);
195
+                $defaultMemberNo = $memberModel ? $memberModel->memberno : '';
196
+
197
+                foreach ($decodedValue as $behaelterNumber) {
198
+                    $result[] = [
199
+                        'behaelter' => $behaelterNumber,
200
+                        'member' => $defaultMemberNo
201
+                    ];
202
+                }
203
+
204
+                return $result;
205
+            }
179 206
         }
180 207
         return $varValue;
181 208
     }
... ...
@@ -185,10 +212,9 @@ class WeinanlieferungReservationContainerListener
185 212
      */
186 213
     public function onBehaelterNumbersSaveCallback($varValue, DataContainer $dc)
187 214
     {
188
-        if (!empty($varValue))
215
+        $varValue = StringUtil::deserialize($varValue, true);
216
+        if (!empty($varValue) && is_array($varValue))
189 217
         {
190
-            $values = array_map('trim', explode(',', $varValue));
191
-
192 218
             // Get the reservation model
193 219
             $reservation = \vonRotenberg\WeinanlieferungBundle\Model\WeinanlieferungReservationModel::findByPk($dc->id);
194 220
             if ($reservation === null) {
... ...
@@ -204,15 +230,20 @@ class WeinanlieferungReservationContainerListener
204 230
             // Check if the booking is in the past
205 231
             $isInPast = $slot->time < time();
206 232
 
233
+            // Extract behaelter numbers from the multiColumnWizard data
234
+            $behaelterNumbers = array_map(function($item) {
235
+                return $item['behaelter'];
236
+            }, $varValue);
237
+
207 238
             // Only perform validation if the booking is not in the past
208 239
             if (!$isInPast) {
209 240
                 // Check if the number of container numbers matches the number of booked containers
210
-                if (count($values) != $reservation->behaelter) {
241
+                if (count($behaelterNumbers) != $reservation->behaelter) {
211 242
                     throw new \Exception('Die Anzahl der Behälternummern muss mit der Anzahl der gebuchten Behälter übereinstimmen.');
212 243
                 }
213 244
 
214 245
                 // Filter out the special value 9999 ("Nummer nicht bekannt") for duplicate check
215
-                $numbersForDuplicateCheck = array_filter($values, function($number) {
246
+                $numbersForDuplicateCheck = array_filter($behaelterNumbers, function($number) {
216 247
                     return $number !== '9999';
217 248
                 });
218 249
 
... ...
@@ -247,8 +278,20 @@ class WeinanlieferungReservationContainerListener
247 278
                 while ($row = $result->fetchAssociative()) {
248 279
                     $numbers = json_decode($row['behaelter_numbers'], true);
249 280
                     if (is_array($numbers)) {
250
-                        foreach ($numbers as $number) {
251
-                            $usedNumbers[] = $number;
281
+                        // Check if we have the new format (array of objects with behaelter and member)
282
+                        // or the old format (simple array of behaelter numbers)
283
+                        $isNewFormat = isset($numbers[0]) && is_array($numbers[0]) && isset($numbers[0]['behaelter']);
284
+
285
+                        if ($isNewFormat) {
286
+                            // Extract just the behaelter numbers from the new format
287
+                            foreach ($numbers as $item) {
288
+                                $usedNumbers[] = $item['behaelter'];
289
+                            }
290
+                        } else {
291
+                            // Old format - simple array of behaelter numbers
292
+                            foreach ($numbers as $number) {
293
+                                $usedNumbers[] = $number;
294
+                            }
252 295
                         }
253 296
                     }
254 297
                 }
... ...
@@ -267,7 +310,27 @@ class WeinanlieferungReservationContainerListener
267 310
                 }
268 311
             }
269 312
 
270
-            $varValue = json_encode($values);
313
+            // Get the member associated with this reservation as fallback
314
+            $memberId = $reservation->uid;
315
+            $memberModel = \Contao\MemberModel::findById($memberId);
316
+            $defaultMemberNo = $memberModel ? $memberModel->memberno : '';
317
+
318
+            // Process the data from the multiColumnWizard
319
+            $processedData = [];
320
+            foreach ($varValue as $item) {
321
+                $behaelterNumber = $item['behaelter'];
322
+
323
+                // If member number is empty, use the default member number
324
+                $memberNumber = !empty($item['member']) ? $item['member'] : $defaultMemberNo;
325
+
326
+                $processedData[] = [
327
+                    'behaelter' => $behaelterNumber,
328
+                    'member' => $memberNumber
329
+                ];
330
+            }
331
+
332
+            // Return the processed data as JSON
333
+            return json_encode($processedData);
271 334
         }
272 335
         return $varValue;
273 336
     }