Browse code

Attributes WIP

Benjamin Roth authored on04/08/2025 14:11:23
Showing23 changed files
1 1
new file mode 100644
... ...
@@ -0,0 +1,187 @@
1
+# Behaelter and Member Number Widget
2
+
3
+This document describes the implementation of a widget for editing both behaelter (container) numbers and their corresponding member numbers in the backend.
4
+
5
+## Overview
6
+
7
+The implementation allows backend users to:
8
+- Edit both behaelter numbers and their corresponding member numbers in a paired interface
9
+- See which member number is associated with each behaelter
10
+- Change the member number for individual behaelters
11
+- Leave the member number field empty to use the booking member's number as a fallback
12
+
13
+## Implementation Details
14
+
15
+### 1. Widget Configuration
16
+
17
+The behaelter_numbers field in `tl_vr_wa_reservation.php` has been updated to use the multiColumnWizard input type with two columns:
18
+- **Behälternummer**: A text input for the behaelter number with validation for natural numbers
19
+- **Mitgliedsnummer**: A text input for the member number with validation for natural numbers
20
+
21
+```php
22
+'behaelter_numbers' => array
23
+(
24
+    'exclude'                 => true,
25
+    'inputType'               => 'multiColumnWizard',
26
+    'eval'                    => array(
27
+        'tl_class' => 'clr',
28
+        'columnFields' => array
29
+        (
30
+            'behaelter' => array
31
+            (
32
+                'label' => $GLOBALS['TL_LANG']['MSC']['wa_behaelter_number'],
33
+                'inputType' => 'text',
34
+                'eval' => array('style' => 'width:200px', 'mandatory' => true, 'rgxp' => 'natural')
35
+            ),
36
+            'member' => array
37
+            (
38
+                'label' => $GLOBALS['TL_LANG']['MSC']['wa_member_number'],
39
+                'inputType' => 'text',
40
+                'eval' => array('style' => 'width:200px', 'rgxp' => 'natural')
41
+            )
42
+        )
43
+    ),
44
+    'sql'                     => "text NULL"
45
+),
46
+```
47
+
48
+### 2. Callback Modifications
49
+
50
+#### Load Callback
51
+
52
+The `onBehaelterNumbersLoadCallback` method has been updated to:
53
+- Handle the new data format (array of objects with behaelter and member)
54
+- Convert old format data (simple array of behaelter numbers) to the new format
55
+- Return the data in the format expected by the multiColumnWizard
56
+
57
+```php
58
+public function onBehaelterNumbersLoadCallback($varValue, DataContainer $dc)
59
+{
60
+    if (!empty($varValue))
61
+    {
62
+        $decodedValue = \json_decode($varValue, true);
63
+
64
+        // Check if we have the new format (array of objects with behaelter and member)
65
+        // or the old format (simple array of behaelter numbers)
66
+        $isNewFormat = isset($decodedValue[0]) && is_array($decodedValue[0]) && isset($decodedValue[0]['behaelter']);
67
+
68
+        if ($isNewFormat) {
69
+            // The data is already in the correct format for the multiColumnWizard
70
+            return $decodedValue;
71
+        } else {
72
+            // Convert old format to new format
73
+            $result = [];
74
+
75
+            // Get the member associated with this reservation as fallback
76
+            $reservation = \vonRotenberg\WeinanlieferungBundle\Model\WeinanlieferungReservationModel::findByPk($dc->id);
77
+            $memberId = $reservation ? $reservation->uid : 0;
78
+            $memberModel = \Contao\MemberModel::findById($memberId);
79
+            $defaultMemberNo = $memberModel ? $memberModel->memberno : '';
80
+
81
+            foreach ($decodedValue as $behaelterNumber) {
82
+                $result[] = [
83
+                    'behaelter' => $behaelterNumber,
84
+                    'member' => $defaultMemberNo
85
+                ];
86
+            }
87
+
88
+            return $result;
89
+        }
90
+    }
91
+    return $varValue;
92
+}
93
+```
94
+
95
+#### Save Callback
96
+
97
+The `onBehaelterNumbersSaveCallback` method has been updated to:
98
+- Handle the data format from the multiColumnWizard (array of rows with behaelter and member)
99
+- Validate the behaelter numbers
100
+- Process the member numbers, using the default member number as a fallback if a member number is empty
101
+- Return the processed data as JSON
102
+
103
+```php
104
+public function onBehaelterNumbersSaveCallback($varValue, DataContainer $dc)
105
+{
106
+    if (!empty($varValue) && is_array($varValue))
107
+    {
108
+        // ... validation code ...
109
+
110
+        // Get the member associated with this reservation as fallback
111
+        $memberId = $reservation->uid;
112
+        $memberModel = \Contao\MemberModel::findById($memberId);
113
+        $defaultMemberNo = $memberModel ? $memberModel->memberno : '';
114
+
115
+        // Process the data from the multiColumnWizard
116
+        $processedData = [];
117
+        foreach ($varValue as $item) {
118
+            $behaelterNumber = $item['behaelter'];
119
+
120
+            // If member number is empty, use the default member number
121
+            $memberNumber = !empty($item['member']) ? $item['member'] : $defaultMemberNo;
122
+
123
+            $processedData[] = [
124
+                'behaelter' => $behaelterNumber,
125
+                'member' => $memberNumber
126
+            ];
127
+        }
128
+
129
+        // Return the processed data as JSON
130
+        return json_encode($processedData);
131
+    }
132
+
133
+    return $varValue;
134
+}
135
+```
136
+
137
+### 3. Language Labels
138
+
139
+Language labels have been added to the default.php language file for the column headers:
140
+
141
+```php
142
+$GLOBALS['TL_LANG']['MSC']['wa_behaelter_number'] = 'Behälternummer';
143
+$GLOBALS['TL_LANG']['MSC']['wa_member_number'] = 'Mitgliedsnummer';
144
+```
145
+
146
+## Data Structure
147
+
148
+The behaelter_numbers field stores a JSON-encoded array of objects, each with:
149
+- `behaelter`: The behaelter number
150
+- `member`: The associated member number
151
+
152
+Example:
153
+```json
154
+[
155
+  {
156
+    "behaelter": "123",
157
+    "member": "456"
158
+  },
159
+  {
160
+    "behaelter": "789",
161
+    "member": "456"
162
+  }
163
+]
164
+```
165
+
166
+## Backward Compatibility
167
+
168
+The implementation maintains backward compatibility with existing data:
169
+- If the behaelter_numbers field contains the old format (simple array of behaelter numbers), it's automatically converted to the new format
170
+- For old data, all behaelters are associated with the booking member's number
171
+- The existing CSV export functionality continues to work with both old and new data formats
172
+
173
+## Usage
174
+
175
+To use the widget:
176
+1. Edit a reservation in the backend
177
+2. Scroll to the "Check-in" section
178
+3. Enter behaelter numbers and optionally member numbers for each behaelter
179
+4. If a member number is left empty, the booking member's number will be used as a fallback
180
+5. Save the reservation
181
+
182
+## Integration with Existing Features
183
+
184
+This widget integrates with the existing features:
185
+- The CSV export functionality uses the member numbers associated with each behaelter
186
+- The backend booking list displays the behaelter numbers with their associated member numbers
187
+- The check-in process in the frontend continues to work with the new data structure
0 188
new file mode 100644
... ...
@@ -0,0 +1,66 @@
1
+# Behaelter Numbers Backend Edit Fix
2
+
3
+This document describes the changes made to fix the backend edit functionality for the `behaelter_numbers` field in the `tl_vr_wa_reservation` table.
4
+
5
+## Issue Description
6
+
7
+The backend edit functionality for the `behaelter_numbers` field was not handling the new data format correctly, which was causing errors. The new data format stores both the behaelter number and the associated member number, while the old format only stored the behaelter numbers.
8
+
9
+## Changes Made
10
+
11
+### 1. WeinanlieferungReservationContainerListener.php
12
+
13
+#### onBehaelterNumbersLoadCallback
14
+
15
+The `onBehaelterNumbersLoadCallback` method was updated to handle the new data format:
16
+
17
+- It now checks if the data is in the new format (array of objects with behaelter and member)
18
+- If it's the new format, it extracts just the behaelter numbers for display in the backend
19
+- If it's the old format, it continues to work as before
20
+- This ensures that the backend edit form displays just the behaelter numbers as a comma-separated list, regardless of the data format
21
+
22
+#### onBehaelterNumbersSaveCallback
23
+
24
+The `onBehaelterNumbersSaveCallback` method was updated to handle the new data format:
25
+
26
+- It now properly handles both the old and new formats when checking used numbers from existing reservations
27
+- It preserves member information when saving by:
28
+  - Checking if the original value is in the new format
29
+  - Creating a map of behaelter number to member number from the original data
30
+  - Getting the member associated with the reservation as a fallback
31
+  - Creating a new data structure that preserves the member information for existing behaelter numbers and uses the default member number for new ones
32
+- This ensures that member information is not lost when editing behaelter numbers in the backend
33
+
34
+## Data Structure
35
+
36
+The `behaelter_numbers` field now stores a JSON-encoded array of objects, each with:
37
+- `behaelter`: The behaelter number
38
+- `member`: The associated member number
39
+
40
+Example:
41
+```json
42
+[
43
+  {
44
+    "behaelter": "123",
45
+    "member": "456"
46
+  },
47
+  {
48
+    "behaelter": "789",
49
+    "member": "456"
50
+  }
51
+]
52
+```
53
+
54
+## Backward Compatibility
55
+
56
+The implementation maintains backward compatibility with existing data:
57
+- If the `behaelter_numbers` field contains the old format (simple array of behaelter numbers), it's automatically handled correctly
58
+- When saving, the old format is converted to the new format, with all behaelters associated with the booking member's number
59
+
60
+## Testing
61
+
62
+The changes were tested with both the old and new data formats to ensure:
63
+- The backend edit form displays just the behaelter numbers as a comma-separated list
64
+- Member information is preserved when saving
65
+- Validation of behaelter numbers works correctly
66
+- The implementation handles both formats correctly when checking used numbers
0 67
new file mode 100644
... ...
@@ -0,0 +1,73 @@
1
+# Member Number for Behaelter Feature
2
+
3
+This document describes the changes made to implement the feature that allows specifying a member number for each booked behaelter during check-in.
4
+
5
+## Overview
6
+
7
+The feature allows users to:
8
+- Specify a different member number for each behaelter during check-in
9
+- If no member number is provided, the system uses the logged-in member's number as a fallback
10
+- The behaelter number and member number are stored together in the existing `behaelter_numbers` database column as a serialized array
11
+
12
+## Changes Made
13
+
14
+### 1. SlotAjaxController.php
15
+
16
+- Added import for `MemberModel` class
17
+- Updated `renderCheckin` method to:
18
+  - Load the member model for the booking's user
19
+  - Load the current logged-in member's model
20
+  - Pass both to the template as `member` and `current_member`
21
+- Updated `updateCheckin` method to:
22
+  - Process member numbers from the form input
23
+  - Implement fallback to use the logged-in member's number if none is provided
24
+  - Store a combined data structure with both behaelter numbers and member numbers
25
+
26
+### 2. modal_checkin.html.twig
27
+
28
+- Added input fields for member numbers alongside each behaelter number select
29
+- Set the current member's number as the default value
30
+- Added helpful text indicating that leaving the field empty will use the current member's number
31
+- Updated the layout to accommodate the new fields
32
+
33
+### 3. CheckInCompletedListener.php
34
+
35
+- Added backward compatibility to handle both the new format (array of objects with behaelter and member) and the old format (simple array of behaelter numbers)
36
+- Updated the CSV export generation to use the member number associated with each behaelter
37
+- Added logic to look up member information based on the member number
38
+
39
+## Data Structure
40
+
41
+The `behaelter_numbers` column now stores a JSON-encoded array of objects, each with:
42
+- `behaelter`: The behaelter number
43
+- `member`: The associated member number
44
+
45
+Example:
46
+```json
47
+[
48
+  {
49
+    "behaelter": "123",
50
+    "member": "456"
51
+  },
52
+  {
53
+    "behaelter": "789",
54
+    "member": "456"
55
+  }
56
+]
57
+```
58
+
59
+## Backward Compatibility
60
+
61
+The implementation maintains backward compatibility with existing data:
62
+- If the `behaelter_numbers` column contains the old format (simple array of behaelter numbers), it's automatically converted to the new format
63
+- For old data, all behaelters are associated with the booking member's number
64
+
65
+## Testing
66
+
67
+To test this feature:
68
+1. Log in as a member
69
+2. Navigate to a booking that can be checked in
70
+3. Click the check-in button
71
+4. Enter behaelter numbers and optionally member numbers
72
+5. Submit the form
73
+6. Verify that the CSV export contains the correct member numbers for each behaelter
0 74
new file mode 100644
... ...
@@ -0,0 +1,89 @@
1
+# Check-In Completed Event
2
+
3
+This document describes the implementation of the check-in completed event in the Weinanlieferung Bundle.
4
+
5
+## Overview
6
+
7
+The check-in completed event is triggered when a reservation check-in is completed. This event allows other parts of the application to react to check-ins, such as sending notifications, updating statistics, or performing other actions.
8
+
9
+## Implementation Details
10
+
11
+### Files Created/Modified
12
+
13
+1. **Created: `src/Event/CheckInCompletedEvent.php`**
14
+   - Defines the event class that is dispatched when a check-in is completed
15
+   - Contains the reservation data and model
16
+   - Provides getter methods to access the data
17
+
18
+2. **Modified: `src/Controller/Frontend/Ajax/SlotAjaxController.php`**
19
+   - Added the event dispatcher as a dependency
20
+   - Updated the constructor to inject the event dispatcher
21
+   - Modified the `updateCheckin()` method to dispatch the event after a successful check-in
22
+
23
+3. **Created: `src/EventListener/CheckInCompletedListener.php`**
24
+   - Example listener that demonstrates how to subscribe to the check-in completed event
25
+   - Logs when a check-in is completed
26
+
27
+## How to Use the Event
28
+
29
+### Listening to the Event
30
+
31
+To listen to the check-in completed event, create a class that implements `EventSubscriberInterface`:
32
+
33
+```php
34
+use Symfony\Component\EventDispatcher\EventSubscriberInterface;
35
+use vonRotenberg\WeinanlieferungBundle\Event\CheckInCompletedEvent;
36
+
37
+class YourListener implements EventSubscriberInterface
38
+{
39
+    public static function getSubscribedEvents()
40
+    {
41
+        return [
42
+            CheckInCompletedEvent::NAME => 'onCheckInCompleted',
43
+        ];
44
+    }
45
+
46
+    public function onCheckInCompleted(CheckInCompletedEvent $event)
47
+    {
48
+        // Get the reservation data
49
+        $reservationData = $event->getReservationData();
50
+        $reservationModel = $event->getReservationModel();
51
+
52
+        // Perform your custom actions here
53
+    }
54
+}
55
+```
56
+
57
+### Available Data
58
+
59
+The event provides access to:
60
+
61
+1. **Reservation Data Array**: Contains all fields from the reservation record
62
+   ```php
63
+   $reservationData = $event->getReservationData();
64
+   $id = $reservationData['id'];
65
+   $checkedIn = $reservationData['checked_in'];
66
+   $checkedInOn = $reservationData['checked_in_on'];
67
+   // etc.
68
+   ```
69
+
70
+2. **Reservation Model**: Provides the Contao model object for the reservation
71
+   ```php
72
+   $reservationModel = $event->getReservationModel();
73
+   $id = $reservationModel->id;
74
+   $slot = $reservationModel->getRelated('pid');
75
+   // etc.
76
+   ```
77
+
78
+## Testing
79
+
80
+The example listener `CheckInCompletedListener` logs when a check-in is completed. You can check the Contao system log to verify that the event is being dispatched correctly.
81
+
82
+When a check-in is completed, you should see a log entry like:
83
+```
84
+Check-in completed for reservation ID: 123
85
+```
86
+
87
+## Conclusion
88
+
89
+This implementation provides a flexible way to react to check-in events in the Weinanlieferung Bundle. By using Symfony's event system, it allows for loose coupling between the check-in process and any additional functionality that needs to be triggered when a check-in occurs.
0 90
new file mode 100644
... ...
@@ -0,0 +1,72 @@
1
+# Check-In CSV Export
2
+
3
+This document describes the implementation of the CSV export functionality for checked-in bookings in the Weinanlieferung Bundle.
4
+
5
+## Overview
6
+
7
+When a reservation is checked in, a CSV export file is automatically generated. The file contains one line for each container (Behälter) in the booking, with detailed information about the member, grape variety, location, harvest type, and more.
8
+
9
+## Implementation Details
10
+
11
+The CSV export functionality is implemented in the `CheckInCompletedListener` class, which listens to the `CheckInCompletedEvent` that is dispatched when a reservation check-in is completed.
12
+
13
+### Files Modified
14
+
15
+1. **Modified: `src/EventListener/CheckInCompletedListener.php`**
16
+   - Updated to generate a CSV export file for each checked-in booking
17
+   - Added functionality to create the export directory if it doesn't exist
18
+   - Implemented the CSV generation logic according to the requirements
19
+
20
+2. **Modified: `config/services.yml`**
21
+   - Added a specific service definition for the CheckInCompletedListener
22
+   - Configured it to inject the project directory parameter
23
+
24
+### CSV File Format
25
+
26
+The CSV file contains one line for each container (Behälter) in the booking, with the following columns:
27
+
28
+1. Member ID
29
+2. Member first and last name (concatenated with space)
30
+3. Rebsorte (grape variety) identifier
31
+4. Rebsorte (grape variety) title
32
+5. Lage (location) identifier
33
+6. Lage (location) title
34
+7. Leseart (harvest type) identifier
35
+8. Leseart (harvest type) title
36
+9. Ernteart (harvest method) - "H" for "handlese" or "V" for "vollernter"
37
+10. Behälter number
38
+11. Check-in date (format d.m.Y)
39
+12. Behälter amount for the whole checked-in booking
40
+
41
+### File Location and Naming
42
+
43
+The CSV files are saved in the `/export/check_in` directory inside the project directory. If these directories don't exist, they are created automatically.
44
+
45
+The filename is constructed using the booking ID and the member ID, separated by an underscore, with a `.csv` extension. For example: `123_456.csv`.
46
+
47
+## How It Works
48
+
49
+1. When a reservation is checked in, the `CheckInCompletedEvent` is dispatched in the `SlotAjaxController`.
50
+2. The `CheckInCompletedListener` receives the event and extracts the reservation data.
51
+3. The listener creates the export directory if it doesn't exist.
52
+4. It retrieves the member data, container numbers, and other required information.
53
+5. For each container, it creates a line in the CSV file with the required data.
54
+6. It saves the CSV file in the export directory with the appropriate filename.
55
+7. The listener logs the successful creation of the CSV file.
56
+
57
+## Error Handling
58
+
59
+The listener includes error handling for the following cases:
60
+
61
+1. If the member data cannot be found, an error is logged and the CSV generation is aborted.
62
+2. If no container numbers are found, an error is logged and the CSV generation is aborted.
63
+3. If a container number is "9999" (special value for "Nummer nicht bekannt"), it is skipped.
64
+
65
+## Testing
66
+
67
+To test the CSV export functionality:
68
+
69
+1. Check in a reservation through the frontend interface.
70
+2. Verify that a CSV file is created in the `/export/check_in` directory.
71
+3. Check the content of the CSV file to ensure it contains the correct data.
72
+4. Check the Contao system log for any errors or success messages related to the CSV export.
... ...
@@ -16,6 +16,8 @@ use vonRotenberg\WeinanlieferungBundle\Model\WeinanlieferungLeseartModel;
16 16
 use vonRotenberg\WeinanlieferungBundle\Model\WeinanlieferungReservationModel;
17 17
 use vonRotenberg\WeinanlieferungBundle\Model\WeinanlieferungSlotsModel;
18 18
 use vonRotenberg\WeinanlieferungBundle\Model\WeinanlieferungStandortModel;
19
+use vonRotenberg\WeinanlieferungBundle\Model\WeinanlieferungAttributeGroupModel;
20
+use vonRotenberg\WeinanlieferungBundle\Model\WeinanlieferungAttributeModel;
19 21
 use Contao\ArrayUtil;
20 22
 
21 23
 ArrayUtil::arrayInsert($GLOBALS['BE_MOD'],1,[
... ...
@@ -27,6 +29,10 @@ ArrayUtil::arrayInsert($GLOBALS['BE_MOD'],1,[
27 29
         'wa_units' => [
28 30
             'tables'     => array('tl_vr_wa_units'),
29 31
             'stylesheet' => array('bundles/vonrotenbergweinanlieferung/css/backend.css')
32
+        ],
33
+        'wa_attributes' => [
34
+            'tables'     => array('tl_vr_wa_attribute_group', 'tl_vr_wa_attribute'),
35
+            'stylesheet' => array('bundles/vonrotenbergweinanlieferung/css/backend.css')
30 36
         ]
31 37
     ]
32 38
 ]);
... ...
@@ -40,6 +46,8 @@ $GLOBALS['TL_MODELS']['tl_vr_wa_rebsorte'] = WeinanlieferungRebsorteModel::class
40 46
 $GLOBALS['TL_MODELS']['tl_vr_wa_leseart'] = WeinanlieferungLeseartModel::class;
41 47
 $GLOBALS['TL_MODELS']['tl_vr_wa_lage'] = WeinanlieferungLageModel::class;
42 48
 $GLOBALS['TL_MODELS']['tl_vr_wa_reservation'] = WeinanlieferungReservationModel::class;
49
+$GLOBALS['TL_MODELS']['tl_vr_wa_attribute_group'] = WeinanlieferungAttributeGroupModel::class;
50
+$GLOBALS['TL_MODELS']['tl_vr_wa_attribute'] = WeinanlieferungAttributeModel::class;
43 51
 
44 52
 
45 53
 // Notification
46 54
new file mode 100644
... ...
@@ -0,0 +1,150 @@
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
+\Contao\System::loadLanguageFile('default');
15
+\Contao\System::loadLanguageFile('tl_vr_wa_attribute');
16
+
17
+$GLOBALS['TL_DCA']['tl_vr_wa_attribute'] = array
18
+(
19
+    // Config
20
+    'config' => array
21
+    (
22
+        'dataContainer'    => DC_Table::class,
23
+        'ptable'           => 'tl_vr_wa_attribute_group',
24
+        'enableVersioning' => true,
25
+        'sql'              => array
26
+        (
27
+            'keys' => array
28
+            (
29
+                'id' => 'primary',
30
+                'pid' => 'index'
31
+            )
32
+        )
33
+    ),
34
+
35
+    // List
36
+    'list' => array
37
+    (
38
+        'sorting' => array
39
+        (
40
+            'mode'                    => DataContainer::MODE_PARENT,
41
+            'fields'                  => array('title'),
42
+            'headerFields'            => array('title'),
43
+            'flag'                    => DataContainer::SORT_INITIAL_LETTER_ASC,
44
+            'panelLayout'             => 'filter;search,limit'
45
+        ),
46
+        'label' => array
47
+        (
48
+            'fields'                  => array('title', 'type'),
49
+            'format'                  => '%s <span style="color:#999;padding-left:3px">[%s]</span>'
50
+        ),
51
+        'global_operations' => array
52
+        (
53
+            'all' => array
54
+            (
55
+                'href'                => 'act=select',
56
+                'class'               => 'header_edit_all',
57
+                'attributes'          => 'onclick="Backend.getScrollOffset()" accesskey="e"'
58
+            )
59
+        ),
60
+        'operations' => array
61
+        (
62
+            'edit' => array
63
+            (
64
+                'href'                => 'act=edit',
65
+                'icon'                => 'edit.gif'
66
+            ),
67
+            'copy' => array
68
+            (
69
+                'href'                => 'act=paste&amp;mode=copy',
70
+                'icon'                => 'copy.svg'
71
+            ),
72
+            'cut' => array
73
+            (
74
+                'href'                => 'act=paste&amp;mode=cut',
75
+                'icon'                => 'cut.svg',
76
+                'attributes'          => 'onclick="Backend.getScrollOffset()"'
77
+            ),
78
+            'delete' => array
79
+            (
80
+                'href'                => 'act=delete',
81
+                'icon'                => 'delete.gif',
82
+                'attributes'          => 'onclick="if(!confirm(\'' . $GLOBALS['TL_LANG']['MSC']['deleteConfirm'] . '\'))return false;Backend.getScrollOffset()"'
83
+            ),
84
+            'show' => array
85
+            (
86
+                'href'                => 'act=show',
87
+                'icon'                => 'show.gif'
88
+            )
89
+        )
90
+    ),
91
+
92
+    // Palettes
93
+    'palettes' => array
94
+    (
95
+        '__selector__'                => array('type'),
96
+        'default'                     => '{title_legend},title,description;{type_legend},type'
97
+    ),
98
+
99
+    // Subpalettes
100
+    'subpalettes' => array
101
+    (
102
+        'type_text'                   => '',
103
+        'type_option'                 => ''
104
+    ),
105
+
106
+    // Fields
107
+    'fields' => array
108
+    (
109
+        'id' => array
110
+        (
111
+            'sql'                     => "int(10) unsigned NOT NULL auto_increment"
112
+        ),
113
+        'pid' => array
114
+        (
115
+            'foreignKey'              => 'tl_vr_wa_attribute_group.title',
116
+            'sql'                     => "int(10) unsigned NOT NULL default '0'",
117
+            'relation'                => array('type'=>'belongsTo', 'load'=>'eager')
118
+        ),
119
+        'tstamp' => array
120
+        (
121
+            'sql'                     => "int(10) unsigned NOT NULL default '0'"
122
+        ),
123
+        'title' => array
124
+        (
125
+            'exclude'                 => true,
126
+            'search'                  => true,
127
+            'inputType'               => 'text',
128
+            'eval'                    => array('mandatory'=>true, 'maxlength'=>255, 'tl_class'=>'w50'),
129
+            'sql'                     => "varchar(255) NOT NULL default ''"
130
+        ),
131
+        'description' => array
132
+        (
133
+            'exclude'                 => true,
134
+            'search'                  => true,
135
+            'inputType'               => 'textarea',
136
+            'eval'                    => array('tl_class'=>'clr'),
137
+            'sql'                     => "text NULL"
138
+        ),
139
+        'type' => array
140
+        (
141
+            'exclude'                 => true,
142
+            'filter'                  => true,
143
+            'inputType'               => 'select',
144
+            'options'                 => array('text', 'option'),
145
+            'reference'               => &$GLOBALS['TL_LANG']['tl_vr_wa_attribute']['type_options'],
146
+            'eval'                    => array('mandatory'=>true, 'submitOnChange'=>true, 'tl_class'=>'w50'),
147
+            'sql'                     => "varchar(32) NOT NULL default 'text'"
148
+        ),
149
+    )
150
+);
0 151
new file mode 100644
... ...
@@ -0,0 +1,135 @@
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
+\Contao\System::loadLanguageFile('default');
15
+\Contao\System::loadLanguageFile('tl_vr_wa_attribute_group');
16
+
17
+$GLOBALS['TL_DCA']['tl_vr_wa_attribute_group'] = array
18
+(
19
+    // Config
20
+    'config' => array
21
+    (
22
+        'dataContainer'    => DC_Table::class,
23
+        'ctable'           => array('tl_vr_wa_attribute'),
24
+        'enableVersioning' => true,
25
+        'sql'              => array
26
+        (
27
+            'keys' => array
28
+            (
29
+                'id' => 'primary'
30
+            )
31
+        )
32
+    ),
33
+
34
+    // List
35
+    'list' => array
36
+    (
37
+        'sorting' => array
38
+        (
39
+            'mode'                    => DataContainer::MODE_SORTED,
40
+            'fields'                  => array('title'),
41
+            'flag'                    => DataContainer::SORT_INITIAL_LETTER_ASC,
42
+            'panelLayout'             => 'filter;search,limit'
43
+        ),
44
+        'label' => array
45
+        (
46
+            'fields'                  => array('title'),
47
+            'format'                  => '%s'
48
+        ),
49
+        'global_operations' => array
50
+        (
51
+            'all' => array
52
+            (
53
+                'href'                => 'act=select',
54
+                'class'               => 'header_edit_all',
55
+                'attributes'          => 'onclick="Backend.getScrollOffset()" accesskey="e"'
56
+            )
57
+        ),
58
+        'operations' => array
59
+        (
60
+            'edit' => array
61
+            (
62
+                'href'                => 'act=edit',
63
+                'icon'                => 'edit.gif'
64
+            ),
65
+            'copy' => array
66
+            (
67
+                'href'                => 'act=copy',
68
+                'icon'                => 'copy.svg'
69
+            ),
70
+            'delete' => array
71
+            (
72
+                'href'                => 'act=delete',
73
+                'icon'                => 'delete.gif',
74
+                'attributes'          => 'onclick="if(!confirm(\'' . $GLOBALS['TL_LANG']['MSC']['deleteConfirm'] . '\'))return false;Backend.getScrollOffset()"'
75
+            ),
76
+            'show' => array
77
+            (
78
+                'href'                => 'act=show',
79
+                'icon'                => 'show.gif'
80
+            ),
81
+            'attributes' => array
82
+            (
83
+                'href'                => 'table=tl_vr_wa_attribute',
84
+                'icon'                => '/bundles/vonrotenbergweinanlieferung/images/icons/layers.svg'
85
+            )
86
+        )
87
+    ),
88
+
89
+    // Palettes
90
+    'palettes' => array
91
+    (
92
+        'default'                     => '{title_legend},title,description;{config_legend},multiple,required'
93
+    ),
94
+
95
+    // Fields
96
+    'fields' => array
97
+    (
98
+        'id' => array
99
+        (
100
+            'sql'                     => "int(10) unsigned NOT NULL auto_increment"
101
+        ),
102
+        'tstamp' => array
103
+        (
104
+            'sql'                     => "int(10) unsigned NOT NULL default '0'"
105
+        ),
106
+        'title' => array
107
+        (
108
+            'exclude'                 => true,
109
+            'inputType'               => 'text',
110
+            'eval'                    => array('mandatory'=>true, 'maxlength'=>255, 'tl_class'=>'w50'),
111
+            'sql'                     => "varchar(255) NOT NULL default ''"
112
+        ),
113
+        'description' => array
114
+        (
115
+            'exclude'                 => true,
116
+            'inputType'               => 'textarea',
117
+            'eval'                    => array('tl_class'=>'clr'),
118
+            'sql'                     => "text NULL"
119
+        ),
120
+        'multiple' => array
121
+        (
122
+            'exclude'                 => true,
123
+            'inputType'               => 'checkbox',
124
+            'eval'                    => array('tl_class'=>'w50 m12'),
125
+            'sql'                     => "char(1) NOT NULL default ''"
126
+        ),
127
+        'required' => array
128
+        (
129
+            'exclude'                 => true,
130
+            'inputType'               => 'checkbox',
131
+            'eval'                    => array('tl_class'=>'w50 m12'),
132
+            'sql'                     => "char(1) NOT NULL default ''"
133
+        )
134
+    )
135
+);
... ...
@@ -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,unit,amount,behaelter,annotation,upload'
100
+        'default' => 'pid,uid,unit,amount,behaelter,attribute_values,annotation,upload'
101 101
     ),
102 102
 
103 103
     // Subpalettes
... ...
@@ -216,6 +216,16 @@ $GLOBALS['TL_DCA']['tl_vr_wa_reservation'] = array
216 216
             ),
217 217
             'sql'                     => "varchar(255) BINARY NOT NULL default ''"
218 218
         ),
219
+        'attribute_values' => array
220
+        (
221
+            'exclude'                 => true,
222
+            'inputType'               => 'textarea',
223
+            'eval'                    => array('tl_class'=>'clr', 'readonly'=>"true", 'allowHtml'=>false, 'preserveTags'=>false),
224
+            'sql'                     => "text NULL",
225
+            'load_callback'           => array(
226
+                array('tl_vr_wa_reservation', 'formatAttributeValues')
227
+            )
228
+        ),
219 229
         'annotation' => array
220 230
         (
221 231
             'exclude'                 => true,
... ...
@@ -93,7 +93,7 @@ $GLOBALS['TL_DCA']['tl_vr_wa_slot'] = array
93 93
     (
94 94
         '__selector__' => array('addEnclosure'),
95 95
 //        'default' => '{time_legend},date,time,duration;{type_legend},behaelter,sorten,lage,ernteart;{info_legend},anmerkungen,addEnclosure;{booking_legend},published,buchbar_ab,buchbar_bis'
96
-        'default' => '{time_legend},date,time,duration;{type_legend},behaelter,overcapacity;{info_legend},anmerkungen,addEnclosure;{booking_legend},published,buchbar_ab,buchbar_bis'
96
+        'default' => '{time_legend},date,time,duration;{type_legend},behaelter,overcapacity;{attributes_legend},attributes;{info_legend},anmerkungen,addEnclosure;{booking_legend},published,buchbar_ab,buchbar_bis'
97 97
     ),
98 98
 
99 99
     // Subpalettes
... ...
@@ -278,5 +278,15 @@ $GLOBALS['TL_DCA']['tl_vr_wa_slot'] = array
278 278
             'eval'      => array('rgxp' => 'datim', 'mandatory' => true, 'datepicker' => true, 'tl_class' => 'w50 wizard'),
279 279
             'sql'       => "int(10) unsigned NULL"
280 280
         ),
281
+        'attributes' => array
282
+        (
283
+            'exclude'                 => true,
284
+            'inputType'               => 'checkboxWizard',
285
+            'eval'                    => array('multiple'=>true, 'csv'=>',', 'tl_class'=>'clr'),
286
+            'load_callback'           => array(array('vonRotenberg\WeinanlieferungBundle\EventListener\DataContainer\WeinanlieferungSlotContainerListener', 'loadAttributes')),
287
+            'save_callback'           => array(array('vonRotenberg\WeinanlieferungBundle\EventListener\DataContainer\WeinanlieferungSlotContainerListener', 'saveAttributes')),
288
+            'options_callback'        => array('vonRotenberg\WeinanlieferungBundle\EventListener\DataContainer\WeinanlieferungSlotContainerListener', 'getAttributeOptions'),
289
+            'sql'                     => "blob NULL"
290
+        ),
281 291
     )
282 292
 );
... ...
@@ -76,5 +76,14 @@ $GLOBALS['TL_DCA']['tl_vr_wa_slotassistant'] =
76 76
                     'eval'                    => ['multiple' =>true, 'fieldType' =>'checkbox', 'filesOnly' =>true, 'isDownloads' =>true, 'extensions' =>Config::get('allowedDownload'), 'mandatory' =>true, 'isSortable' =>true],
77 77
                     'sql'                     => "blob NULL"
78 78
                 ],
79
+                'attributes' => [
80
+                    'exclude'                 => true,
81
+                    'inputType'               => 'checkboxWizard',
82
+                    'eval'                    => ['multiple'=>true, 'csv'=>',', 'tl_class'=>'clr'],
83
+                    'load_callback'           => [['vonRotenberg\WeinanlieferungBundle\EventListener\DataContainer\WeinanlieferungSlotContainerListener', 'loadAttributes']],
84
+                    'save_callback'           => [['vonRotenberg\WeinanlieferungBundle\EventListener\DataContainer\WeinanlieferungSlotContainerListener', 'saveAttributes']],
85
+                    'options_callback'        => ['vonRotenberg\WeinanlieferungBundle\EventListener\DataContainer\WeinanlieferungSlotContainerListener', 'getAttributeOptions'],
86
+                    'sql'                     => "blob NULL"
87
+                ],
79 88
             ]
80 89
     ];
81 90
new file mode 100644
... ...
@@ -0,0 +1,28 @@
1
+<?php
2
+
3
+/**
4
+ * This file is part of contao-weinanlieferung-bundle.
5
+ *
6
+ * (c) vonRotenberg
7
+ *
8
+ * @license commercial
9
+ */
10
+
11
+$GLOBALS['TL_LANG']['tl_vr_wa_attribute']['title_legend'] = 'Titel und Beschreibung';
12
+$GLOBALS['TL_LANG']['tl_vr_wa_attribute']['type_legend'] = 'Attributtyp';
13
+
14
+$GLOBALS['TL_LANG']['tl_vr_wa_attribute']['title'] = ['Titel', 'Bitte geben Sie den Titel des Attributs ein.'];
15
+$GLOBALS['TL_LANG']['tl_vr_wa_attribute']['description'] = ['Beschreibung', 'Bitte geben Sie eine Beschreibung des Attributs ein.'];
16
+$GLOBALS['TL_LANG']['tl_vr_wa_attribute']['type'] = ['Typ', 'Bitte wählen Sie den Typ des Attributs.'];
17
+
18
+$GLOBALS['TL_LANG']['tl_vr_wa_attribute']['type_options'] = [
19
+    'text' => 'Freitext',
20
+    'option' => 'Option (Auswählbar)'
21
+];
22
+
23
+$GLOBALS['TL_LANG']['tl_vr_wa_attribute']['new'] = ['Neues Attribut', 'Ein neues Attribut erstellen'];
24
+$GLOBALS['TL_LANG']['tl_vr_wa_attribute']['edit'] = ['Attribut bearbeiten', 'Attribut ID %s bearbeiten'];
25
+$GLOBALS['TL_LANG']['tl_vr_wa_attribute']['copy'] = ['Attribut duplizieren', 'Attribut ID %s duplizieren'];
26
+$GLOBALS['TL_LANG']['tl_vr_wa_attribute']['cut'] = ['Attribut verschieben', 'Attribut ID %s verschieben'];
27
+$GLOBALS['TL_LANG']['tl_vr_wa_attribute']['delete'] = ['Attribut löschen', 'Attribut ID %s löschen'];
28
+$GLOBALS['TL_LANG']['tl_vr_wa_attribute']['show'] = ['Attribut anzeigen', 'Details des Attributs ID %s anzeigen'];
0 29
new file mode 100644
... ...
@@ -0,0 +1,24 @@
1
+<?php
2
+
3
+/**
4
+ * This file is part of contao-weinanlieferung-bundle.
5
+ *
6
+ * (c) vonRotenberg
7
+ *
8
+ * @license commercial
9
+ */
10
+
11
+$GLOBALS['TL_LANG']['tl_vr_wa_attribute_group']['title_legend'] = 'Titel und Beschreibung';
12
+$GLOBALS['TL_LANG']['tl_vr_wa_attribute_group']['config_legend'] = 'Konfiguration';
13
+
14
+$GLOBALS['TL_LANG']['tl_vr_wa_attribute_group']['title'] = ['Titel', 'Bitte geben Sie den Titel der Attributgruppe ein.'];
15
+$GLOBALS['TL_LANG']['tl_vr_wa_attribute_group']['description'] = ['Beschreibung', 'Bitte geben Sie eine Beschreibung der Attributgruppe ein.'];
16
+$GLOBALS['TL_LANG']['tl_vr_wa_attribute_group']['multiple'] = ['Mehrfachauswahl', 'Erlaubt die Auswahl mehrerer Attribute aus dieser Gruppe.'];
17
+$GLOBALS['TL_LANG']['tl_vr_wa_attribute_group']['required'] = ['Pflichtfeld', 'Die Auswahl eines Attributs aus dieser Gruppe ist erforderlich.'];
18
+
19
+$GLOBALS['TL_LANG']['tl_vr_wa_attribute_group']['new'] = ['Neue Attributgruppe', 'Eine neue Attributgruppe erstellen'];
20
+$GLOBALS['TL_LANG']['tl_vr_wa_attribute_group']['edit'] = ['Attributgruppe bearbeiten', 'Attributgruppe ID %s bearbeiten'];
21
+$GLOBALS['TL_LANG']['tl_vr_wa_attribute_group']['copy'] = ['Attributgruppe duplizieren', 'Attributgruppe ID %s duplizieren'];
22
+$GLOBALS['TL_LANG']['tl_vr_wa_attribute_group']['delete'] = ['Attributgruppe löschen', 'Attributgruppe ID %s löschen'];
23
+$GLOBALS['TL_LANG']['tl_vr_wa_attribute_group']['show'] = ['Attributgruppe anzeigen', 'Details der Attributgruppe ID %s anzeigen'];
24
+$GLOBALS['TL_LANG']['tl_vr_wa_attribute_group']['attributes'] = ['Attribute verwalten', 'Attribute der Gruppe ID %s verwalten'];
... ...
@@ -26,6 +26,8 @@ $GLOBALS['TL_LANG']['tl_vr_wa_reservation']['uid'][0] = 'Winzer';
26 26
 $GLOBALS['TL_LANG']['tl_vr_wa_reservation']['uid'][1] = 'Zuordnung des anliefernden Winzers.';
27 27
 $GLOBALS['TL_LANG']['tl_vr_wa_reservation']['upload'][0] = 'Datei';
28 28
 $GLOBALS['TL_LANG']['tl_vr_wa_reservation']['upload'][1] = 'Die hochgeladene Datei des Winzers.';
29
+$GLOBALS['TL_LANG']['tl_vr_wa_reservation']['attribute_values'][0] = 'Attributwerte';
30
+$GLOBALS['TL_LANG']['tl_vr_wa_reservation']['attribute_values'][1] = 'Die vom Buchenden ausgewählten Attributwerte.';
29 31
 $GLOBALS['TL_LANG']['tl_vr_wa_reservation']['annotation'][0] = 'Anmerkungen';
30 32
 $GLOBALS['TL_LANG']['tl_vr_wa_reservation']['annotation'][1] = 'Ergänzende Hinweise zu diesem Zeitslot.';
31 33
 
... ...
@@ -40,9 +40,12 @@ $GLOBALS['TL_LANG']['tl_vr_wa_slot']['buchbar_ab'][0] = 'Buchbar ab';
40 40
 $GLOBALS['TL_LANG']['tl_vr_wa_slot']['buchbar_ab'][1] = 'Zeitpunkt, ab wann der Slot gebucht werden kann.';
41 41
 $GLOBALS['TL_LANG']['tl_vr_wa_slot']['buchbar_bis'][0] = 'Buchbar bis';
42 42
 $GLOBALS['TL_LANG']['tl_vr_wa_slot']['buchbar_bis'][1] = 'Zeitpunkt, bis wann der Slot spätestens gebucht werden kann.';
43
+$GLOBALS['TL_LANG']['tl_vr_wa_slot']['attribute_groups'][0] = 'Attributgruppen';
44
+$GLOBALS['TL_LANG']['tl_vr_wa_slot']['attribute_groups'][1] = 'Wählen Sie die Attributgruppen aus, die für diesen Slot verfügbar sein sollen.';
43 45
 
44 46
 $GLOBALS['TL_LANG']['tl_vr_wa_slot']['time_legend'] = 'Zeit';
45 47
 $GLOBALS['TL_LANG']['tl_vr_wa_slot']['type_legend'] = 'Verarbeitungs-Einstellungen';
48
+$GLOBALS['TL_LANG']['tl_vr_wa_slot']['attributes_legend'] = 'Attribute';
46 49
 $GLOBALS['TL_LANG']['tl_vr_wa_slot']['info_legend'] = 'Zusätzliche Informationen';
47 50
 $GLOBALS['TL_LANG']['tl_vr_wa_slot']['booking_legend'] = 'Buchbarkeits-Einstellungen';
48 51
 
... ...
@@ -22,6 +22,14 @@
22 22
                 <source>Slot settings</source>
23 23
                 <target>Slot-Einstellungen</target>
24 24
             </trans-unit>
25
+            <trans-unit id="tl_vr_wa_slotassistant.attribute_groups">
26
+                <source>Attribute groups</source>
27
+                <target>Attributgruppen</target>
28
+            </trans-unit>
29
+            <trans-unit id="tl_vr_wa_slotassistant.attributes_legend">
30
+                <source>Attributes</source>
31
+                <target>Attribute</target>
32
+            </trans-unit>
25 33
         </body>
26 34
     </file>
27 35
 </xliff>
... ...
@@ -77,6 +77,39 @@
77 77
                         </fieldset>
78 78
                     </div>
79 79
                 </div>
80
+                {% if attribute_groups is defined and attribute_groups|length > 0 %}
81
+                    {% for group in attribute_groups %}
82
+                        <fieldset>
83
+                            <legend><strong>{{ group.title }}{% if group.required %}<sup class="text-danger">*</sup>{% endif %}</strong></legend>
84
+                            {% if group.description %}
85
+                                <div class="description">{{ group.description }}</div>
86
+                            {% endif %}
87
+
88
+                            {% for attribute in group.attributes %}
89
+                                <div class="attribute-item">
90
+                                    {% if attribute.type == 'text' %}
91
+                                        <label for="attribute_{{ attribute.id }}">{{ attribute.title }}</label>
92
+                                        <input type="text" id="attribute_{{ attribute.id }}" name="attribute_{{ attribute.id }}" class="form-control" value="{{ attribute.value }}">
93
+                                        {% if attribute.description %}
94
+                                            <div class="description">{{ attribute.description }}</div>
95
+                                        {% endif %}
96
+                                    {% elseif attribute.type == 'option' %}
97
+                                        <div class="checkbox">
98
+                                            <label>
99
+                                                <input type="checkbox" id="attribute_{{ attribute.id }}" name="attribute_{{ attribute.id }}" value="1" {% if attribute.value %}checked{% endif %}>
100
+                                                {{ attribute.title }}
101
+                                            </label>
102
+                                            {% if attribute.description %}
103
+                                                <div class="description">{{ attribute.description }}</div>
104
+                                            {% endif %}
105
+                                        </div>
106
+                                    {% endif %}
107
+                                </div>
108
+                            {% endfor %}
109
+                        </fieldset>
110
+                    {% endfor %}
111
+                {% endif %}
112
+
80 113
                 <fieldset>
81 114
                     <label for="res-annotation"><strong>Anmerkung</strong></label>
82 115
                     <textarea id="res-annotation" name="annotation">{{ buchung.annotation }}</textarea>
... ...
@@ -88,12 +121,14 @@
88 121
                     </fieldset>
89 122
                     <fieldset>
90 123
                         <label for="res-upload"><strong>Datei überschreiben</strong></label>
124
+                        <input type="file" id="res-upload" name="upload">
125
+                    </fieldset>
91 126
                 {% else %}
92 127
                     <fieldset>
93 128
                         <label for="res-upload"><strong>Datei hochladen</strong></label>
129
+                        <input type="file" id="res-upload" name="upload">
130
+                    </fieldset>
94 131
                 {% endif %}
95
-                    <input type="file" id="res-upload" name="upload">
96
-                </fieldset>
97 132
                 <fieldset>
98 133
                     <button type="submit">Speichern</button>
99 134
                 </fieldset>
... ...
@@ -101,6 +136,7 @@
101 136
         </div>
102 137
 
103 138
         <div class="loader animated loading"></div>
139
+    </div>
104 140
     {% if modal %}</div>{% endif %}
105 141
 {% endblock %}
106 142
 
... ...
@@ -111,6 +111,39 @@
111 111
                                 </fieldset>
112 112
                             </div>
113 113
                         </div>
114
+                        {% if attribute_groups is defined and attribute_groups|length > 0 %}
115
+                            {% for group in attribute_groups %}
116
+                                <fieldset>
117
+                                    <legend><strong>{{ group.title }}{% if group.required %}<sup class="text-danger">*</sup>{% endif %}</strong></legend>
118
+                                    {% if group.description %}
119
+                                        <div class="description">{{ group.description }}</div>
120
+                                    {% endif %}
121
+
122
+                                    {% for attribute in group.attributes %}
123
+                                        <div class="attribute-item">
124
+                                            {% if attribute.type == 'text' %}
125
+                                                <label for="attribute_{{ attribute.id }}">{{ attribute.title }}</label>
126
+                                                <input type="text" id="attribute_{{ attribute.id }}" name="attribute_{{ attribute.id }}" class="form-control">
127
+                                                {% if attribute.description %}
128
+                                                    <div class="description">{{ attribute.description }}</div>
129
+                                                {% endif %}
130
+                                            {% elseif attribute.type == 'option' %}
131
+                                                <div class="checkbox">
132
+                                                    <label>
133
+                                                        <input type="checkbox" id="attribute_{{ attribute.id }}" name="attribute_{{ attribute.id }}" value="1">
134
+                                                        {{ attribute.title }}
135
+                                                    </label>
136
+                                                    {% if attribute.description %}
137
+                                                        <div class="description">{{ attribute.description }}</div>
138
+                                                    {% endif %}
139
+                                                </div>
140
+                                            {% endif %}
141
+                                        </div>
142
+                                    {% endfor %}
143
+                                </fieldset>
144
+                            {% endfor %}
145
+                        {% endif %}
146
+
114 147
                         <fieldset>
115 148
                             <label for="res-annotation"><strong>Anmerkung</strong></label>
116 149
                             <textarea id="res-annotation" name="annotation"></textarea>
... ...
@@ -37,6 +37,8 @@ use vonRotenberg\WeinanlieferungBundle\Model\WeinanlieferungRebsorteModel;
37 37
 use vonRotenberg\WeinanlieferungBundle\Model\WeinanlieferungReservationModel;
38 38
 use vonRotenberg\WeinanlieferungBundle\Model\WeinanlieferungSlotsModel;
39 39
 use vonRotenberg\WeinanlieferungBundle\Model\WeinanlieferungUnitsModel;
40
+use vonRotenberg\WeinanlieferungBundle\Model\WeinanlieferungAttributeGroupModel;
41
+use vonRotenberg\WeinanlieferungBundle\Model\WeinanlieferungAttributeModel;
40 42
 
41 43
 /**
42 44
  * @Route("/_ajax/vr_wa/v1/slot", name="vr_wa_slot_ajax", defaults={"_scope" = "frontend", "_token_check" = false})
... ...
@@ -147,7 +149,69 @@ class SlotAjaxController extends AbstractController
147 149
                     $arrUnits[$unit->id] = $unit->title . ' (' . $unit->containers . ' '.$this->translator->trans('tl_vr_wa_units.containers.0', [], 'contao_tl_vr_wa_units').')';
148 150
                 }
149 151
             }
152
+        }
153
+
154
+        // Load attribute groups and attributes
155
+        $arrAttributeGroups = [];
156
+
157
+        // Get selected attributes
158
+        $selectedAttributeIds = [];
159
+        if ($Slot->attributes) {
160
+            $selectedAttributeIds = StringUtil::deserialize($Slot->attributes, true);
161
+        }
162
+
163
+        // If no attributes are selected, don't show any
164
+        if (empty($selectedAttributeIds)) {
165
+            // Keep the attribute_groups array empty
166
+        } else {
167
+            // Fetch the selected attributes
168
+            $selectedAttributes = WeinanlieferungAttributeModel::findMultipleByIds($selectedAttributeIds);
169
+            if ($selectedAttributes !== null) {
170
+                // Group the attributes by their parent group
171
+                $attributesByGroup = [];
172
+                $groupIds = [];
173
+
174
+                // First pass: collect all group IDs and organize attributes by group
175
+                foreach ($selectedAttributes as $attribute) {
176
+                    $groupId = $attribute->pid;
177
+                    $groupIds[] = $groupId;
178
+
179
+                    if (!isset($attributesByGroup[$groupId])) {
180
+                        $attributesByGroup[$groupId] = [];
181
+                    }
182
+
183
+                    $attributesByGroup[$groupId][] = [
184
+                        'id' => $attribute->id,
185
+                        'title' => $attribute->title,
186
+                        'description' => $attribute->description,
187
+                        'type' => $attribute->type
188
+                    ];
189
+                }
150 190
 
191
+                // Get unique group IDs
192
+                $groupIds = array_unique($groupIds);
193
+
194
+                // Fetch the groups
195
+                if (!empty($groupIds)) {
196
+                    $groups = WeinanlieferungAttributeGroupModel::findMultipleByIds($groupIds);
197
+
198
+                    if ($groups !== null) {
199
+                        // Create the attribute groups array
200
+                        foreach ($groups as $group) {
201
+                            if (isset($attributesByGroup[$group->id])) {
202
+                                $arrAttributeGroups[] = [
203
+                                    'id' => $group->id,
204
+                                    'title' => $group->title,
205
+                                    'description' => $group->description,
206
+                                    'multiple' => (bool)$group->multiple,
207
+                                    'required' => (bool)$group->required,
208
+                                    'attributes' => $attributesByGroup[$group->id]
209
+                                ];
210
+                            }
211
+                        }
212
+                    }
213
+                }
214
+            }
151 215
         }
152 216
 
153 217
         $arrData = [
... ...
@@ -163,7 +227,8 @@ class SlotAjaxController extends AbstractController
163 227
                 'behaelter' => range(min($intBookableBehaelter,1),$intBookableBehaelter),
164 228
                 'units' => $arrUnits,
165 229
             ],
166
-            'reservations' => $arrReservations
230
+            'reservations' => $arrReservations,
231
+            'attribute_groups' => $arrAttributeGroups
167 232
         ];
168 233
 
169 234
         if (!empty($error))
... ...
@@ -322,7 +387,83 @@ class SlotAjaxController extends AbstractController
322 387
                 $intDefaultAmount = floor($intDefaultAmount / max(1, $Unit->containers));
323 388
                 $intUnitAmount = floor($intUnitAmount / max(1, $Unit->containers));
324 389
             }
390
+        }
391
+
392
+        // Load attribute groups and attributes
393
+        $arrAttributeGroups = [];
394
+        $attributeValues = [];
395
+
396
+        // Parse existing attribute values if available
397
+        if (!empty($Booking->attribute_values)) {
398
+            try {
399
+                $attributeValues = json_decode($Booking->attribute_values, true);
400
+                if (!is_array($attributeValues)) {
401
+                    $attributeValues = [];
402
+                }
403
+            } catch (\Exception $e) {
404
+                $attributeValues = [];
405
+            }
406
+        }
325 407
 
408
+        // Get selected attributes
409
+        $selectedAttributeIds = [];
410
+        if ($Slot->attributes) {
411
+            $selectedAttributeIds = StringUtil::deserialize($Slot->attributes, true);
412
+        }
413
+
414
+        // If no attributes are selected, don't show any
415
+        if (empty($selectedAttributeIds)) {
416
+            // Keep the attribute_groups array empty
417
+        } else {
418
+            // Fetch the selected attributes
419
+            $selectedAttributes = WeinanlieferungAttributeModel::findMultipleByIds($selectedAttributeIds);
420
+            if ($selectedAttributes !== null) {
421
+                // Group the attributes by their parent group
422
+                $attributesByGroup = [];
423
+                $groupIds = [];
424
+
425
+                // First pass: collect all group IDs and organize attributes by group
426
+                foreach ($selectedAttributes as $attribute) {
427
+                    $groupId = $attribute->pid;
428
+                    $groupIds[] = $groupId;
429
+
430
+                    if (!isset($attributesByGroup[$groupId])) {
431
+                        $attributesByGroup[$groupId] = [];
432
+                    }
433
+
434
+                    $attributesByGroup[$groupId][] = [
435
+                        'id' => $attribute->id,
436
+                        'title' => $attribute->title,
437
+                        'description' => $attribute->description,
438
+                        'type' => $attribute->type,
439
+                        'value' => $attributeValues[$attribute->id] ?? null
440
+                    ];
441
+                }
442
+
443
+                // Get unique group IDs
444
+                $groupIds = array_unique($groupIds);
445
+
446
+                // Fetch the groups
447
+                if (!empty($groupIds)) {
448
+                    $groups = WeinanlieferungAttributeGroupModel::findMultipleByIds($groupIds);
449
+
450
+                    if ($groups !== null) {
451
+                        // Create the attribute groups array
452
+                        foreach ($groups as $group) {
453
+                            if (isset($attributesByGroup[$group->id])) {
454
+                                $arrAttributeGroups[] = [
455
+                                    'id' => $group->id,
456
+                                    'title' => $group->title,
457
+                                    'description' => $group->description,
458
+                                    'multiple' => (bool)$group->multiple,
459
+                                    'required' => (bool)$group->required,
460
+                                    'attributes' => $attributesByGroup[$group->id]
461
+                                ];
462
+                            }
463
+                        }
464
+                    }
465
+                }
466
+            }
326 467
         }
327 468
 
328 469
         $arrData = array_merge($arrData,[
... ...
@@ -339,7 +480,8 @@ class SlotAjaxController extends AbstractController
339 480
                 'default' => $intDefaultAmount,
340 481
                 'behaelter' => $intUnitAmount ? range(1,$intUnitAmount) : [],
341 482
                 'units' => $arrUnits,
342
-            ]
483
+            ],
484
+            'attribute_groups' => $arrAttributeGroups
343 485
         ]);
344 486
 
345 487
         if (!empty($error))
... ...
@@ -425,6 +567,83 @@ class SlotAjaxController extends AbstractController
425 567
             $intBehaelter = intval($intBehaelter) * intval($SelectedUnit->containers);
426 568
         }
427 569
 
570
+        // Process attribute values
571
+        $attributeValues = [];
572
+
573
+        // Get selected attributes
574
+        $selectedAttributeIds = [];
575
+        if ($Slot->attributes) {
576
+            $selectedAttributeIds = StringUtil::deserialize($Slot->attributes, true);
577
+        }
578
+
579
+        // If no attributes are selected, skip attribute processing
580
+        if (!empty($selectedAttributeIds)) {
581
+            // Fetch the selected attributes
582
+            $selectedAttributes = WeinanlieferungAttributeModel::findMultipleByIds($selectedAttributeIds);
583
+            if ($selectedAttributes !== null) {
584
+                // Group the attributes by their parent group
585
+                $attributesByGroup = [];
586
+                $groupIds = [];
587
+
588
+                // First pass: collect all group IDs
589
+                foreach ($selectedAttributes as $attribute) {
590
+                    $groupId = $attribute->pid;
591
+                    $groupIds[] = $groupId;
592
+                }
593
+
594
+                // Get unique group IDs
595
+                $groupIds = array_unique($groupIds);
596
+
597
+                // Fetch the groups
598
+                if (!empty($groupIds)) {
599
+                    $groups = WeinanlieferungAttributeGroupModel::findMultipleByIds($groupIds);
600
+
601
+                    if ($groups !== null) {
602
+                        // Create maps for attribute-to-group and group-required status
603
+                        $attributeToGroupMap = [];
604
+                        $groupRequiredMap = [];
605
+
606
+                        foreach ($groups as $group) {
607
+                            $groupRequiredMap[$group->id] = (bool)$group->required;
608
+                        }
609
+
610
+                        foreach ($selectedAttributes as $attribute) {
611
+                            $attributeToGroupMap[$attribute->id] = $attribute->pid;
612
+                        }
613
+
614
+                        // Track which groups have values
615
+                        $groupHasValue = [];
616
+
617
+                        // Process the selected attributes
618
+                        foreach ($selectedAttributes as $attribute) {
619
+                            $attributeKey = 'attribute_' . $attribute->id;
620
+                            $groupId = $attributeToGroupMap[$attribute->id];
621
+
622
+                            if ($attribute->type === 'text') {
623
+                                // For text attributes, store the text value
624
+                                if (!empty(Input::post($attributeKey))) {
625
+                                    $attributeValues[$attribute->id] = Input::post($attributeKey);
626
+                                    $groupHasValue[$groupId] = true;
627
+                                }
628
+                            } else if ($attribute->type === 'option') {
629
+                                // For option attributes, store true if selected
630
+                                if (Input::post($attributeKey)) {
631
+                                    $attributeValues[$attribute->id] = true;
632
+                                    $groupHasValue[$groupId] = true;
633
+                                }
634
+                            }
635
+                        }
636
+
637
+                        // Check if required groups have values
638
+                        foreach ($groupRequiredMap as $groupId => $isRequired) {
639
+                            if ($isRequired && empty($groupHasValue[$groupId])) {
640
+                                $arrError[] = 'attribute_group_' . $groupId;
641
+                            }
642
+                        }
643
+                    }
644
+                }
645
+            }
646
+        }
428 647
 
429 648
         if (count($arrError))
430 649
         {
... ...
@@ -440,6 +659,7 @@ class SlotAjaxController extends AbstractController
440 659
             'behaelter' => $intBehaelter,
441 660
             'amount' => Input::post('behaelter'),
442 661
             'annotation' => Input::post('annotation'),
662
+            'attribute_values' => !empty($attributeValues) ? json_encode($attributeValues) : null,
443 663
         ]);
444 664
         $Reservation->setRow($arrData);
445 665
         $Reservation->save();
... ...
@@ -526,6 +746,84 @@ class SlotAjaxController extends AbstractController
526 746
             $intBehaelter = intval($intBehaelter) * intval($SelectedUnit->containers);
527 747
         }
528 748
 
749
+        // Process attribute values
750
+        $attributeValues = [];
751
+
752
+        // Get selected attributes
753
+        $selectedAttributeIds = [];
754
+        if ($Slot->attributes) {
755
+            $selectedAttributeIds = StringUtil::deserialize($Slot->attributes, true);
756
+        }
757
+
758
+        // If no attributes are selected, skip attribute processing
759
+        if (!empty($selectedAttributeIds)) {
760
+            // Fetch the selected attributes
761
+            $selectedAttributes = WeinanlieferungAttributeModel::findMultipleByIds($selectedAttributeIds);
762
+            if ($selectedAttributes !== null) {
763
+                // Group the attributes by their parent group
764
+                $attributesByGroup = [];
765
+                $groupIds = [];
766
+
767
+                // First pass: collect all group IDs
768
+                foreach ($selectedAttributes as $attribute) {
769
+                    $groupId = $attribute->pid;
770
+                    $groupIds[] = $groupId;
771
+                }
772
+
773
+                // Get unique group IDs
774
+                $groupIds = array_unique($groupIds);
775
+
776
+                // Fetch the groups
777
+                if (!empty($groupIds)) {
778
+                    $groups = WeinanlieferungAttributeGroupModel::findMultipleByIds($groupIds);
779
+
780
+                    if ($groups !== null) {
781
+                        // Create maps for attribute-to-group and group-required status
782
+                        $attributeToGroupMap = [];
783
+                        $groupRequiredMap = [];
784
+
785
+                        foreach ($groups as $group) {
786
+                            $groupRequiredMap[$group->id] = (bool)$group->required;
787
+                        }
788
+
789
+                        foreach ($selectedAttributes as $attribute) {
790
+                            $attributeToGroupMap[$attribute->id] = $attribute->pid;
791
+                        }
792
+
793
+                        // Track which groups have values
794
+                        $groupHasValue = [];
795
+
796
+                        // Process the selected attributes
797
+                        foreach ($selectedAttributes as $attribute) {
798
+                            $attributeKey = 'attribute_' . $attribute->id;
799
+                            $groupId = $attributeToGroupMap[$attribute->id];
800
+
801
+                            if ($attribute->type === 'text') {
802
+                                // For text attributes, store the text value
803
+                                if (!empty(Input::post($attributeKey))) {
804
+                                    $attributeValues[$attribute->id] = Input::post($attributeKey);
805
+                                    $groupHasValue[$groupId] = true;
806
+                                }
807
+                            } else if ($attribute->type === 'option') {
808
+                                // For option attributes, store true if selected
809
+                                if (Input::post($attributeKey)) {
810
+                                    $attributeValues[$attribute->id] = true;
811
+                                    $groupHasValue[$groupId] = true;
812
+                                }
813
+                            }
814
+                        }
815
+
816
+                        // Check if required groups have values
817
+                        foreach ($groupRequiredMap as $groupId => $isRequired) {
818
+                            if ($isRequired && empty($groupHasValue[$groupId])) {
819
+                                $arrError[] = 'attribute_group_' . $groupId;
820
+                            }
821
+                        }
822
+                    }
823
+                }
824
+            }
825
+        }
826
+
529 827
         if (count($arrError))
530 828
         {
531 829
             return $this->renderBooking(false,'<div class="toast toast--danger mx-0">Bitte geben Sie alle Pflichtangaben (mit * markierte Felder) an</div>');
... ...
@@ -536,6 +834,7 @@ class SlotAjaxController extends AbstractController
536 834
         $Reservation->behaelter = $intBehaelter;
537 835
         $Reservation->amount = Input::post('behaelter');
538 836
         $Reservation->annotation = Input::post('annotation');
837
+        $Reservation->attribute_values = !empty($attributeValues) ? json_encode($attributeValues) : null;
539 838
 
540 839
         $Reservation->save();
541 840
 
... ...
@@ -19,6 +19,7 @@ use Contao\Input;
19 19
 use Contao\StringUtil;
20 20
 use Doctrine\DBAL\Connection;
21 21
 use Symfony\Contracts\Translation\TranslatorInterface;
22
+use vonRotenberg\WeinanlieferungBundle\Model\WeinanlieferungAttributeModel;
22 23
 use vonRotenberg\WeinanlieferungBundle\Model\WeinanlieferungLeseartModel;
23 24
 use vonRotenberg\WeinanlieferungBundle\Model\WeinanlieferungRebsorteModel;
24 25
 use vonRotenberg\WeinanlieferungBundle\Model\WeinanlieferungSlotsModel;
... ...
@@ -200,4 +201,53 @@ class WeinanlieferungReservationContainerListener
200 201
 
201 202
         return $varValue;
202 203
     }
204
+
205
+    /**
206
+     * Format attribute values for display in the backend
207
+     *
208
+     * @Callback(table="tl_vr_wa_reservation", target="fields.attribute_values.load")
209
+     */
210
+    public function formatAttributeValues($varValue, DataContainer $dc)
211
+    {
212
+        if (empty($varValue)) {
213
+            return '';
214
+        }
215
+
216
+        try {
217
+            $attributeValues = json_decode($varValue, true);
218
+
219
+            if (!is_array($attributeValues) || empty($attributeValues)) {
220
+                return '';
221
+            }
222
+
223
+            $formattedValues = [];
224
+
225
+            foreach ($attributeValues as $attributeId => $value) {
226
+                // Get attribute information
227
+                $attribute = WeinanlieferungAttributeModel::findByPk($attributeId);
228
+
229
+                if ($attribute === null) {
230
+                    $formattedValues[] = $attributeId . ': ' . (is_array($value) ? implode(', ', $value) : $value);
231
+                    continue;
232
+                }
233
+
234
+                // Format based on attribute type
235
+                $attributeTitle = $attribute->title;
236
+
237
+                if ($attribute->type === 'option') {
238
+                    // For option type, the value is either true (selected) or false (not selected)
239
+                    if ($value === true || $value === 'true' || $value === '1' || $value === 1) {
240
+                        $formattedValues[] = $attributeTitle . ': Ausgewählt';
241
+                    }
242
+                } else {
243
+                    // Text type or fallback
244
+                    $formattedValues[] = $attributeTitle . ': ' . $value;
245
+                }
246
+            }
247
+
248
+            return implode("\n", $formattedValues);
249
+        } catch (\Exception $e) {
250
+            return 'Error parsing attribute values: ' . $e->getMessage();
251
+        }
252
+    }
203 253
 }
... ...
@@ -24,6 +24,8 @@ use vonRotenberg\WeinanlieferungBundle\Model\WeinanlieferungLeseartModel;
24 24
 use vonRotenberg\WeinanlieferungBundle\Model\WeinanlieferungRebsorteModel;
25 25
 use vonRotenberg\WeinanlieferungBundle\Model\WeinanlieferungSlotsModel;
26 26
 use vonRotenberg\WeinanlieferungBundle\Model\WeinanlieferungSlottypesModel;
27
+use vonRotenberg\WeinanlieferungBundle\Model\WeinanlieferungAttributeGroupModel;
28
+use vonRotenberg\WeinanlieferungBundle\Model\WeinanlieferungAttributeModel;
27 29
 use vonRotenberg\WeinanlieferungBundle\SlotChecker;
28 30
 
29 31
 class WeinanlieferungSlotContainerListener
... ...
@@ -130,4 +132,72 @@ class WeinanlieferungSlotContainerListener
130 132
 
131 133
         return $varValue;
132 134
     }
135
+
136
+    /**
137
+     * Load callback for the attributes field
138
+     *
139
+     * @param mixed $varValue The current value
140
+     * @param DataContainer $dc The data container
141
+     * @return mixed The processed value
142
+     */
143
+    public function loadAttributes($varValue, DataContainer $dc)
144
+    {
145
+        // Just return the value as is, it's already in the correct format (comma-separated list of attribute IDs)
146
+        return $varValue;
147
+    }
148
+
149
+    /**
150
+     * Save callback for the attributes field
151
+     *
152
+     * @param mixed $varValue The value to save
153
+     * @param DataContainer $dc The data container
154
+     * @return mixed The processed value
155
+     */
156
+    public function saveAttributes($varValue, DataContainer $dc)
157
+    {
158
+        // Just return the value as is, it's already in the correct format (comma-separated list of attribute IDs)
159
+        return $varValue;
160
+    }
161
+
162
+    /**
163
+     * Options callback for the attributes field
164
+     * Returns a multi-dimensional array of attribute options grouped by their parent groups
165
+     *
166
+     * @param DataContainer $dc The data container
167
+     * @return array The options array
168
+     */
169
+    public function getAttributeOptions(DataContainer $dc)
170
+    {
171
+        System::loadLanguageFile('tl_vr_wa_attribute');
172
+
173
+        // Fetch all attribute groups
174
+        $groups = WeinanlieferungAttributeGroupModel::findAll(['order' => 'title ASC']);
175
+        if ($groups === null) {
176
+            return [];
177
+        }
178
+
179
+        // Build the multi-dimensional array of options
180
+        $options = [];
181
+
182
+        foreach ($groups as $group) {
183
+            // Get the attributes for this group
184
+            $attributes = WeinanlieferungAttributeModel::findBy(['pid=?'], [$group->id], ['order' => 'title ASC']);
185
+            if ($attributes === null) {
186
+                continue;
187
+            }
188
+
189
+            // Add attributes to the options array
190
+            $groupAttributes = [];
191
+            foreach ($attributes as $attribute) {
192
+                $groupAttributes[$attribute->id] = $attribute->title . ' <span style="color:#999;padding-left:3px">[' . $GLOBALS['TL_LANG']['tl_vr_wa_attribute']['type_options'][$attribute->type] . ']</span>';
193
+            }
194
+
195
+            // Add the group to the options array if it has attributes
196
+            if (!empty($groupAttributes)) {
197
+                $options[$group->title] = $groupAttributes;
198
+            }
199
+        }
200
+
201
+        return $options;
202
+    }
133 203
 }
134 204
new file mode 100644
... ...
@@ -0,0 +1,24 @@
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 WeinanlieferungAttributeGroupModel extends Model
18
+{
19
+    /**
20
+     * Table name
21
+     * @var string
22
+     */
23
+    protected static $strTable = 'tl_vr_wa_attribute_group';
24
+}
0 25
new file mode 100644
... ...
@@ -0,0 +1,24 @@
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 WeinanlieferungAttributeModel extends Model
18
+{
19
+    /**
20
+     * Table name
21
+     * @var string
22
+     */
23
+    protected static $strTable = 'tl_vr_wa_attribute';
24
+}