Browse code

First draft of booked slot check-in

Benjamin Roth authored on25/07/2025 09:39:15
Showing5 changed files
... ...
@@ -97,7 +97,7 @@ $GLOBALS['TL_DCA']['tl_vr_wa_reservation'] = array
97 97
     'palettes' => array
98 98
     (
99 99
         '__selector__' => array(),
100
-        'default' => 'pid,uid,behaelter,sorten,lage,ernteart,upload;{approval_legend},approved'
100
+        'default' => 'pid,uid,behaelter,sorten,lage,ernteart,upload;{approval_legend},approved;{checkin_legend},checked_in,behaelter_numbers'
101 101
     ),
102 102
 
103 103
     // Subpalettes
... ...
@@ -201,5 +201,26 @@ $GLOBALS['TL_DCA']['tl_vr_wa_reservation'] = array
201 201
             'eval'                    => array('rgxp' => 'datim', 'datepicker' => true, 'tl_class' => 'clr w50 wizard'),
202 202
             'sql'                     => "int(10) unsigned NOT NULL default '0'"
203 203
         ),
204
+        'checked_in' => array
205
+        (
206
+            'exclude'                 => true,
207
+            'inputType'               => 'checkbox',
208
+            'eval'                    => array('tl_class'=>'w50 m12'),
209
+            'sql'                     => "char(1) NOT NULL default ''"
210
+        ),
211
+        'checked_in_on' => array
212
+        (
213
+            'exclude'                 => true,
214
+            'inputType'               => 'text',
215
+            'eval'                    => array('rgxp' => 'datim', 'datepicker' => true, 'tl_class' => 'clr w50 wizard'),
216
+            'sql'                     => "int(10) unsigned NOT NULL default '0'"
217
+        ),
218
+        'behaelter_numbers' => array
219
+        (
220
+            'exclude'                 => true,
221
+            'inputType'               => 'text',
222
+            'eval'                    => array('tl_class'=>'clr', 'preserveTags'=>true),
223
+            'sql'                     => "text NULL"
224
+        ),
204 225
     )
205 226
 );
... ...
@@ -24,8 +24,15 @@ $GLOBALS['TL_LANG']['tl_vr_wa_reservation']['upload'][0] = 'Datei';
24 24
 $GLOBALS['TL_LANG']['tl_vr_wa_reservation']['upload'][1] = 'Die hochgeladene Datei des Winzers.';
25 25
 $GLOBALS['TL_LANG']['tl_vr_wa_reservation']['approved'][0] = 'Freigabe';
26 26
 $GLOBALS['TL_LANG']['tl_vr_wa_reservation']['approved'][1] = 'Der Freigabe-Status der Buchung.';
27
+$GLOBALS['TL_LANG']['tl_vr_wa_reservation']['checked_in'][0] = 'Eingecheckt';
28
+$GLOBALS['TL_LANG']['tl_vr_wa_reservation']['checked_in'][1] = 'Markiert die Buchung als eingecheckt.';
29
+$GLOBALS['TL_LANG']['tl_vr_wa_reservation']['checked_in_on'][0] = 'Eingecheckt am';
30
+$GLOBALS['TL_LANG']['tl_vr_wa_reservation']['checked_in_on'][1] = 'Zeitpunkt des Check-ins.';
31
+$GLOBALS['TL_LANG']['tl_vr_wa_reservation']['behaelter_numbers'][0] = 'Behälternummern';
32
+$GLOBALS['TL_LANG']['tl_vr_wa_reservation']['behaelter_numbers'][1] = 'Die zugewiesenen Nummern für jeden Behälter.';
27 33
 
28 34
 $GLOBALS['TL_LANG']['tl_vr_wa_reservation']['title_legend'] = 'Leseart';
29 35
 $GLOBALS['TL_LANG']['tl_vr_wa_reservation']['approval_legend'] = 'Freigabe-Einstellungen';
36
+$GLOBALS['TL_LANG']['tl_vr_wa_reservation']['checkin_legend'] = 'Check-in-Informationen';
30 37
 
31 38
 $GLOBALS['TL_LANG']['tl_vr_wa_reservation']['back'] = 'Zurück';
... ...
@@ -102,7 +102,12 @@
102 102
                                     </div>
103 103
 
104 104
                                     <div class="col u-text-right u-text-nowrap action">
105
-                                        {% if booking.approved != '0' %}<a hx-get="/_ajax/vr_wa/v1/slot?do=booking&id={{ booking.id }}" hx-target="body" hx-swap="beforeend" href="javascript:;" class="btn btn--sm btn-info m-0">Ändern</a>{% endif %}
105
+                                        {% if booking.approved != '0' %}
106
+                                            <a hx-get="/_ajax/vr_wa/v1/slot?do=booking&id={{ booking.id }}" hx-target="body" hx-swap="beforeend" href="javascript:;" class="btn btn--sm btn-info m-0">Ändern</a>
107
+                                            {% if not booking.checked_in %}
108
+                                                <a hx-get="/_ajax/vr_wa/v1/slot?do=checkin&id={{ booking.id }}" hx-target="body" hx-swap="beforeend" href="javascript:;" class="btn btn--sm btn-success m-0">Check-in</a>
109
+                                            {% endif %}
110
+                                        {% endif %}
106 111
                                         <a hx-get="/_ajax/vr_wa/v1/slot?do=delete&id={{ booking.id }}" hx-target="body" hx-swap="beforeend" hx-confirm="Sind Sie sicher, dass Sie diese Reservierung löschen möchten?" href="javascript:;" class="btn btn--sm btn-danger m-0">Löschen</a>
107 112
                                     </div>
108 113
                                 </div>
... ...
@@ -93,6 +93,15 @@ class SlotAjaxController extends AbstractController
93 93
 
94 94
             case 'delete':
95 95
                 return $this->deleteReservation();
96
+
97
+            case 'checkin':
98
+                return $this->renderCheckin($blnModal);
99
+
100
+            case 'updateCheckin':
101
+                return $this->updateCheckin();
102
+
103
+            case 'getAvailableNumbers':
104
+                return $this->getAvailableNumbers();
96 105
         }
97 106
 
98 107
         return new Response('',500);
... ...
@@ -665,4 +674,210 @@ class SlotAjaxController extends AbstractController
665 674
         return $this->render('@Contao/modal_unauthorized.html.twig');
666 675
     }
667 676
 
677
+    protected function renderCheckin(bool $blnModal=true, string $error=null)
678
+    {
679
+        $insertTagService = Controller::getContainer()->get('contao.insert_tag.parser');
680
+
681
+        $arrData = [];
682
+
683
+        if (empty($_REQUEST['id']))
684
+        {
685
+            return new Response('Required parameter missing', 412);
686
+        }
687
+
688
+        /** @var WeinanlieferungReservationModel $Booking */
689
+        if (($Booking = WeinanlieferungReservationModel::findById($_REQUEST['id'])) === null || ($Slot = $Booking->getRelated('pid')) === null)
690
+        {
691
+            return new Response('Could not load booking data', 500);
692
+        }
693
+
694
+        if ($Booking->approved === '0')
695
+        {
696
+            return $this->render('@Contao/modal_message.html.twig', ['type'=>'danger', 'content'=>'Diese Buchungsanfrage wurde abgelehnt und kann nicht eingecheckt werden.']);
697
+        }
698
+
699
+        if ($Booking->checked_in === '1')
700
+        {
701
+            return $this->render('@Contao/modal_message.html.twig', ['type'=>'info', 'content'=>'Diese Buchung wurde bereits eingecheckt.']);
702
+        }
703
+
704
+        // Get the standort to access the number_ranges
705
+        $Standort = $Slot->getRelated('pid');
706
+        if ($Standort === null)
707
+        {
708
+            return new Response('Could not load standort data', 500);
709
+        }
710
+
711
+        // Prepare data for the template
712
+        $arrSortenBooked = [];
713
+        $SortenLeseart = explode(';', $Booking->sorten);
714
+        foreach($SortenLeseart as $sorteLeseart)
715
+        {
716
+            list($sorte, $leseart) = explode(',', $sorteLeseart);
717
+            $objSorte = WeinanlieferungRebsorteModel::findByPk($sorte);
718
+            $objLeseart = WeinanlieferungLeseartModel::findByPk($leseart);
719
+            $arrSortenBooked[$objSorte->id.','.$objLeseart->id] = ($objSorte !== null ? $objSorte->title : '') . ' ' . ($objLeseart !== null ? $objLeseart->title : '');
720
+        }
721
+
722
+        $arrErnteartBooked = [];
723
+        if ($Booking->ernteart !== null)
724
+        {
725
+            foreach (explode(',', $Booking->ernteart) as $ernteart)
726
+            {
727
+                $arrErnteartBooked[] = $GLOBALS['TL_LANG']['REF']['wa_ernteart'][$ernteart] ?? $ernteart;
728
+            }
729
+        }
730
+
731
+        $arrLagenBooked = [];
732
+        if ($Booking->lage !== null)
733
+        {
734
+            if (($Lagen = $Booking->getRelated('lage')) !== null)
735
+            {
736
+                $arrLagenBooked = $Lagen->fetchEach('title');
737
+            }
738
+        }
739
+
740
+        $arrData = array_merge($arrData, [
741
+            'modal' => $blnModal,
742
+            'id' => $Booking->id,
743
+            'slot' => array_merge($Slot->row(), [
744
+                'anmerkungen' => $insertTagService->replace($Slot->anmerkungen ?? ''),
745
+            ]),
746
+            'buchung' => array_merge($Booking->row(), [
747
+                'sorten' => $arrSortenBooked,
748
+                'ernteart' => $arrErnteartBooked,
749
+                'lage' => $arrLagenBooked,
750
+            ]),
751
+            'standort' => $Standort,
752
+            'checkin' => [
753
+                'behaelter' => $Booking->behaelter,
754
+            ]
755
+        ]);
756
+
757
+        if (!empty($error))
758
+        {
759
+            $arrData['toast'] = $error;
760
+        }
761
+
762
+        return $this->render('@Contao/modal_checkin.html.twig', $arrData);
763
+    }
764
+
765
+    protected function updateCheckin()
766
+    {
767
+        if (empty($_REQUEST['id']))
768
+        {
769
+            return new Response('Missing parameter', 412);
770
+        }
771
+
772
+        /** @var WeinanlieferungReservationModel $Booking */
773
+        if (($Booking = WeinanlieferungReservationModel::findById($_REQUEST['id'])) === null)
774
+        {
775
+            return new Response('Could not load booking data', 500);
776
+        }
777
+
778
+        // Validate that we have the correct number of behaelter numbers
779
+        $behaelterNumbers = Input::post('behaelter_numbers');
780
+        if (!is_array($behaelterNumbers) || count($behaelterNumbers) != $Booking->behaelter)
781
+        {
782
+            return $this->renderCheckin(false, '<div class="toast toast--danger mx-0">Bitte wählen Sie für jeden Behälter eine Nummer aus.</div>');
783
+        }
784
+
785
+        // Filter out the special value 9999 ("Nummer nicht bekannt") for duplicate check
786
+        $numbersForDuplicateCheck = array_filter($behaelterNumbers, function($number) {
787
+            return $number !== '9999';
788
+        });
789
+
790
+        // Check for duplicate numbers (excluding the special value 9999)
791
+        if (count(array_unique($numbersForDuplicateCheck)) != count($numbersForDuplicateCheck))
792
+        {
793
+            return $this->renderCheckin(false, '<div class="toast toast--danger mx-0">Jede Nummer kann nur einmal verwendet werden.</div>');
794
+        }
795
+
796
+        // Save the check-in data
797
+        $time = time();
798
+        $Booking->checked_in = '1';
799
+        $Booking->checked_in_on = $time;
800
+        $Booking->behaelter_numbers = json_encode($behaelterNumbers);
801
+        $Booking->tstamp = $time;
802
+        $Booking->save();
803
+
804
+        return new Response('<div class="toast toast--success mx-0"><p>Check-in erfolgreich durchgeführt</p></div>', 200, ['HX-Trigger'=> 'updateWaBooking']);
805
+    }
806
+
807
+    protected function getAvailableNumbers()
808
+    {
809
+        if (empty($_REQUEST['id']))
810
+        {
811
+            return new Response('Required parameter missing', 412);
812
+        }
813
+
814
+        /** @var WeinanlieferungReservationModel $Booking */
815
+        if (($Booking = WeinanlieferungReservationModel::findById($_REQUEST['id'])) === null || ($Slot = $Booking->getRelated('pid')) === null)
816
+        {
817
+            return new Response('Could not load booking data', 500);
818
+        }
819
+
820
+        if ($Booking->approved === '0')
821
+        {
822
+            return new Response(json_encode(['error' => 'Diese Buchungsanfrage wurde abgelehnt und kann nicht eingecheckt werden.']), 400, ['Content-Type' => 'application/json']);
823
+        }
824
+
825
+        if ($Booking->checked_in === '1')
826
+        {
827
+            return new Response(json_encode(['error' => 'Diese Buchung wurde bereits eingecheckt.']), 400, ['Content-Type' => 'application/json']);
828
+        }
829
+
830
+        // Get the standort to access the number_ranges
831
+        $Standort = $Slot->getRelated('pid');
832
+        if ($Standort === null)
833
+        {
834
+            return new Response(json_encode(['error' => 'Could not load standort data']), 500, ['Content-Type' => 'application/json']);
835
+        }
836
+
837
+        // Get all used numbers from current bookings (excluding past bookings)
838
+        $usedNumbers = [];
839
+        $currentTime = time();
840
+
841
+        // Get the database connection
842
+        $db = Controller::getContainer()->get('database_connection');
843
+
844
+        // More efficient query to get used numbers from current bookings
845
+        $sql = "SELECT r.behaelter_numbers
846
+                FROM tl_vr_wa_reservation r
847
+                JOIN tl_vr_wa_slot s ON r.pid = s.id
848
+                WHERE r.behaelter_numbers != ''
849
+                AND s.time >= ?
850
+                AND r.id != ?";
851
+
852
+        $stmt = $db->prepare($sql);
853
+        $stmt->bindValue(1, $currentTime);
854
+        $stmt->bindValue(2, $Booking->id);
855
+        $result = $stmt->executeQuery();
856
+
857
+        while ($row = $result->fetchAssociative()) {
858
+            $numbers = json_decode($row['behaelter_numbers'], true);
859
+            if (is_array($numbers)) {
860
+                foreach ($numbers as $number) {
861
+                    $usedNumbers[] = $number;
862
+                }
863
+            }
864
+        }
865
+
866
+        // Get a batch of available numbers
867
+        // We'll limit to a reasonable number to improve performance
868
+        $limit = 100; // Adjust this based on your needs
869
+        if (!empty($_REQUEST['limit']) && is_numeric($_REQUEST['limit'])) {
870
+            $limit = (int)$_REQUEST['limit'];
871
+        }
872
+
873
+        // Get available numbers directly, excluding used ones
874
+        $availableNumbers = $Standort->extractNumbersFromRanges($usedNumbers, $limit);
875
+
876
+        // Add the special option "Nummer nicht bekannt" with value 9999
877
+        // This option should always be available and can be used multiple times
878
+        array_unshift($availableNumbers, '9999');
879
+
880
+        // Return the numbers as JSON
881
+        return new Response(json_encode(['numbers' => $availableNumbers]), 200, ['Content-Type' => 'application/json']);
882
+    }
668 883
 }
... ...
@@ -21,4 +21,136 @@ class WeinanlieferungStandortModel extends Model
21 21
      * @var string
22 22
      */
23 23
     protected static $strTable = 'tl_vr_wa_standort';
24
+
25
+    /**
26
+     * Extracts all possible numbers from the number_ranges field.
27
+     * Expands ranges like "0000-0002" into individual numbers: "0000", "0001", "0002".
28
+     *
29
+     * @param array $excludeNumbers Optional array of numbers to exclude from the result
30
+     * @param int $limit Optional limit to return only a specific number of results
31
+     * @return array Array of extracted numbers
32
+     */
33
+    public function extractNumbersFromRanges(array $excludeNumbers = [], int $limit = 0): array
34
+    {
35
+        $result = [];
36
+        $ranges = $this->number_ranges;
37
+        $excludeLookup = array_flip($excludeNumbers); // For faster lookups
38
+
39
+        if (empty($ranges)) {
40
+            return $result;
41
+        }
42
+
43
+        // Split by line breaks and other common separators
44
+        $lines = preg_split('/[\r\n,;]+/', $ranges);
45
+
46
+        foreach ($lines as $line) {
47
+            $line = trim($line);
48
+            if (empty($line)) {
49
+                continue;
50
+            }
51
+
52
+            // Check if it's a range (contains a hyphen)
53
+            if (strpos($line, '-') !== false) {
54
+                list($start, $end) = array_map('trim', explode('-', $line, 2));
55
+
56
+                // Ensure both start and end are valid
57
+                if (is_numeric($start) && is_numeric($end)) {
58
+                    // Get the padding length from the start number
59
+                    $padLength = strlen($start);
60
+
61
+                    // Convert to integers for the range calculation
62
+                    $startNum = intval($start);
63
+                    $endNum = intval($end);
64
+
65
+                    // Generate numbers in the range, but check limit
66
+                    for ($i = $startNum; $i <= $endNum; $i++) {
67
+                        $number = str_pad((string)$i, $padLength, '0', STR_PAD_LEFT);
68
+
69
+                        // Skip if this number should be excluded
70
+                        if (isset($excludeLookup[$number])) {
71
+                            continue;
72
+                        }
73
+
74
+                        $result[] = $number;
75
+
76
+                        // Check if we've reached the limit
77
+                        if ($limit > 0 && count($result) >= $limit) {
78
+                            return $result;
79
+                        }
80
+                    }
81
+                } else {
82
+                    // If not a valid range, treat as a single number
83
+                    if (!isset($excludeLookup[$line])) {
84
+                        $result[] = $line;
85
+
86
+                        // Check if we've reached the limit
87
+                        if ($limit > 0 && count($result) >= $limit) {
88
+                            return $result;
89
+                        }
90
+                    }
91
+                }
92
+            } else {
93
+                // It's a single number
94
+                if (!isset($excludeLookup[$line])) {
95
+                    $result[] = $line;
96
+
97
+                    // Check if we've reached the limit
98
+                    if ($limit > 0 && count($result) >= $limit) {
99
+                        return $result;
100
+                    }
101
+                }
102
+            }
103
+        }
104
+
105
+        return $result;
106
+    }
107
+
108
+    /**
109
+     * Returns the count of all possible numbers from the number_ranges field
110
+     * without actually generating all the numbers.
111
+     *
112
+     * @return int Count of all possible numbers
113
+     */
114
+    public function countNumbersInRanges(): int
115
+    {
116
+        $count = 0;
117
+        $ranges = $this->number_ranges;
118
+
119
+        if (empty($ranges)) {
120
+            return $count;
121
+        }
122
+
123
+        // Split by line breaks and other common separators
124
+        $lines = preg_split('/[\r\n,;]+/', $ranges);
125
+
126
+        foreach ($lines as $line) {
127
+            $line = trim($line);
128
+            if (empty($line)) {
129
+                continue;
130
+            }
131
+
132
+            // Check if it's a range (contains a hyphen)
133
+            if (strpos($line, '-') !== false) {
134
+                list($start, $end) = array_map('trim', explode('-', $line, 2));
135
+
136
+                // Ensure both start and end are valid
137
+                if (is_numeric($start) && is_numeric($end)) {
138
+                    // Convert to integers for the range calculation
139
+                    $startNum = intval($start);
140
+                    $endNum = intval($end);
141
+
142
+                    // Add the count of numbers in this range
143
+                    $count += ($endNum - $startNum + 1);
144
+                } else {
145
+                    // If not a valid range, count as a single number
146
+                    $count++;
147
+                }
148
+            } else {
149
+                // It's a single number
150
+                $count++;
151
+            }
152
+        }
153
+
154
+        return $count;
155
+    }
24 156
 }